はじめに
この記事はLinkbal (リンクバル) Advent Calendar 2021の15日目の記事です。
Webの開発業界ではDockerレイヤー キャッシュ(DLC) より、Docker
の概念が慣れた方が多いと思います。本日は、Docker レイヤー キャッシュ(DLC) に関して、Dockerビルドの速度を向上する仕組みを調べてみましょう。
まずDockerについてちょっと調べましょう?
Dockerの仕組みをイメージすると「仮想化」という概念が出てきます。「仮想化」は1台のOSにたくさんの仮想化ソフトウェアをインストールすることにより、複数のサーバとして利用できるものです。ただ、完全な仮想化ソフトウェアを含むのではなく、「コンテナ型」の仮想化物を含んでいます。
Dockerがどのように動くかをみてみます。
簡単なDockerコンテナを作成します。
- Dockerfileを作成し、ソースコードと一緒に追加します。
- DockerfileをDockerビルドコマンドに渡します。
- Dockerは、Dockerfileからの命令を実行することにより、コンテナーのイメージを構築します。
- Dockerは、イメージでコンテナーを作成します。
Dockerfileは下記のコードを記載。
FROM node:lts-alpine
USER node
COPY package.json /home/node/
WORKDIR /home/node
RUN npm install
package.jsonはこんな感じです。
{
"name": "nodejs-demo-docker",
"version": "1.0.0",
"description": "Hello World demo app",
"scripts": {
"start": "echo \"Start demo app\" && exit 0"
}
}
Dockerfileの各行は、Dockerイメージを構築する手段を示しています。
たとえば、COPYコマンドは、指定したファイル・ディレクトリをローカルDockerコンテキスト(ソースコードなど)からコンテナー内にコピーします。 (コンテナには独自のファイルシステムがあります!)
Dockerは順番なコマンドを次々に実行してから、最後に結果のスナップショットを作成して、イメージとして保存します。
保存したイメージからコンテナを作成し、コンテナを起動して、その中でコマンドを実行できます。
下記のDockerビルドコマンドを実行して、定義したDockerfileでイメージを作成します。
docker build --tag demo_app .
docker run demo_app npm start
docker run
は1つのコマンドで作成、開始、実行を組み合わせたようなものです。
Docker レイヤーって何か。
実際に、Dockerはイメージのビルドの最後にスナップショットを作成するだけではくて。Dockerはビルドの実行中に各コマンド結果毎に該当なスナップショットを作成します。
これらのスナップショットはレイヤーと呼ばれ、各レイヤーはコマンドを実行した結果として何が変更されたかを記録したものです。
レイヤーは前のコマンドで変更されたものだけを保存するため、各レイヤーは前のレイヤー(その親レイヤー)に依存します。 親のないレイヤーは無効なレイヤーです。
したがって、レイヤーは次のもので構成されます。
- コマンド
- コマンドで変更された記録
- レイヤーID
- 親レイヤーID
イメージからコンテナーを作成すると、Dockerは各レイヤーからの変更を重ねて再生します(そのため、レイヤーと呼ばれます)。
Dockerビルドの際に、Docker レイヤー キャッシュ(DLC) はがどのように動くか。
docker buildを実行するときに、-cache-fromコマンドライン引数を追加することで、Dockerにレイヤーキャッシュとして使用するイメージを与えることができます。
Dockerは、各命令を実行する前に、実行する命令と一致するレイヤーがキャッシュにあるかどうかを確認し、見つかった場合は、新しいレイヤーを作成するのではなく、そのレイヤーを使用します。 いいね、Docker!
ただし、重要なのは、Dockerは前のレイヤーのIDがキャッシュされたレイヤーの親IDと一致することも確認することです。 前のレイヤーが一致しない場合、キャッシュされたレイヤーは使用できません。
これにより、Dockerが1つの命令のレイヤーキャッシュで一致するものを見つけられなくなるとすぐに、その命令のレイヤーとすべての子レイヤーを再構築する必要があります。キャッシュされた1つのレイヤーを無効にすると、すべてが事実上無効になります。 その子供たちも。
これが実際にどのように機能するかを示すために、次のようなDockerfileの例に戻りましょう。
各コマンドの後、キャッシュ元のレイヤーのIDと共にUsing cache
のロッグを記録されます。
ここで、package.jsonをちょっと変更してビルドを再実行すると、次のようになります。
{
"name": "nodejs-demo-docker",
"version": "1.0.0",
"description": "Hello World demo app",
"scripts": {
"start": "echo \"Start demo app\" && exit 0",
"test": "echo \"Test demo app \" && exit 0"
}
}
Dockerビルドの再実行の結果
前回の結果に比べて、実行時間がかかり、記録したログ旨も異なるところも出てきました。最初の数行は以前と同じですが、package.json
ファイルの変更のせいで、Step4からキャッシュ元のレイヤーを使用しなく、新しいレイヤーを作成されました。
もちろん、これは良いことです。ソースコードを変更して、Dockerに別のイメージを作成させたいからです。 Dockerがキャッシュされたレイヤーを使用したら、間違った結果になります。
ただし、ここで重要なのは、レイヤーのIDも変更されていることです(c737e09e7910からb7d9e0630540に)。 このコマンドから、Dockerがレイヤーキャッシュを調べたときに、一致するレイヤーが見つからないこと認識され、レイヤーのIDが以前とはすべて異なることになります。
変更されたレイヤー以降のすべてのレイヤーを再構築する必要があります。ログ記録でキャッシュが無効になり、Using cache
行はなくなります。
Dockerレイヤー キャッシュ(DLC)の理解はなぜか必要か。
Dockerfileを作成するときは、Dockerレイヤーキャッシングがどのように動くかを理解するのは重要です。
このDockerfileの例をみてみます。
FROM node:lts-alpine
USER node
WORKDIR /home/node
COPY package.json /home/node/
COPY . /home/node/
RUN npm install
ここでは、npm install
コマンドの前にCOPY . /home/node/
コマンドを配置しました。 つまり、ソースコードに存在しているファイルが変更されると、COPY . /home/node/
行でレイヤーキャッシュが無効になります。なので、Rootディレクトリ内のファイルを変更するたびにnpm install
を再度実行してしまいます。 実際には、package.json
ファイルが変更されない限り、npm install
の実行が必要ないです。
そのため、毎回npm install
コマンドの実行を待たずにソースコードを変更できるように下記のようにコマンドの順番を訂正します。
FROM node:lts-alpine
USER node
WORKDIR /home/node
COPY package.json /home/node/
RUN npm install
COPY . /home/node/
ここで、Rootディレクトリ内容を変更したが、package.json
に触れていない場合、Dockerは最初の数レイヤーをキャッシュから直接取得でき、COPY . /home/node/
を再実行するだけで終わります。
Dockerレイヤー キャッシュ(DLC)の仕組みが理解すれば、Dockerコマンドの要らない実行とビルド工数が減らすように出来ます。