イメージビルドのベストプラクティス
読む時間の目安: 5 分
セキュリティスキャン
イメージをビルドしたらdocker scan
コマンドを実行して、イメージにセキュリティぜい弱性がないかどうかをスキャンする。
これがベストプラクティスです。
Docker は Snyk 社と連携してセキュリティスキャンサービスを提供しています。
メモ
イメージをスキャンするには、Docker Hub にログインしておくことが必要です。
docker scan --login
コマンドの実行後に、docker scan <image-name>
を実行して、イメージのスキャンを行います。
たとえば本チュートリアルの初期の頃に生成したgetting-started
イメージをスキャンする場合は、以下のようにします。
$ docker scan getting-started
スキャン処理では常時更新されているぜい弱性データベースを利用しています。 したがって処理結果は、新たなぜい弱性が発見されるたびに変わります。 出力例は以下のようなものです。
✗ Low severity vulnerability found in freetype/freetype
Description: CVE-2020-15999
Info: https://snyk.io/vuln/SNYK-ALPINE310-FREETYPE-1019641
Introduced through: freetype/freetype@2.10.0-r0, gd/libgd@2.2.5-r2
From: freetype/freetype@2.10.0-r0
From: gd/libgd@2.2.5-r2 > freetype/freetype@2.10.0-r0
Fixed in: 2.10.0-r1
✗ Medium severity vulnerability found in libxml2/libxml2
Description: Out-of-bounds Read
Info: https://snyk.io/vuln/SNYK-ALPINE310-LIBXML2-674791
Introduced through: libxml2/libxml2@2.9.9-r3, libxslt/libxslt@1.1.33-r3, nginx-module-xslt/nginx-module-xslt@1.17.9-r1
From: libxml2/libxml2@2.9.9-r3
From: libxslt/libxslt@1.1.33-r3 > libxml2/libxml2@2.9.9-r3
From: nginx-module-xslt/nginx-module-xslt@1.17.9-r1 > libxml2/libxml2@2.9.9-r3
Fixed in: 2.9.9-r4
出力結果にはぜい弱性の種類、詳細 URL が示されます。 そして最も重要なのが対象となるライブラリです。 そこには対象ライブラリのどのバージョンがぜい弱性を解消しているかが示されます。
その他にもいくつかオプションがあります。 その詳細については docker scan のドキュメント を参照してください。
新たに作り出したイメージに対するスキャンをコマンドラインから行う方法と、さらに Docker Hub の設定 を行って新規にプッシュされたイメージを自動的にスキャンすることも可能です。 その場合の処理結果は Docker Hub と Docker Desktop のいずれからでも確認できます。
イメージのレイヤー管理
イメージがどのように構成されているかを確認できるのはご存知でしたか?
docker image history
コマンドを実行すれば、イメージ内のコンテナーがどのようなコマンドによって生成されたかを確認することができます。
-
docker image history
コマンドを実行して、本チュートリアルの初期に生成したgetting-started
イメージ内のレイヤーを確認してみます。$ docker image history getting-started
結果として以下のような出力が得られます(日付や ID は異なるはずです)。
IMAGE CREATED CREATED BY SIZE COMMENT a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j… 0B f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593… 198kB 9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B <missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B <missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B <missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui… 5.35MB <missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B <missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu… 74.3MB <missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B <missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 13 days ago /bin/sh -c #(nop) ADD file:e69d441d729412d24… 5.59MB
各行はイメージ内のレイヤーを表わしています。 この出力結果はベースイメージが最下段に、そして最も新しいレイヤーが最上段に示されます。 このコマンドを用いると、各レイヤーのサイズを簡単に知ることができるので、大容量イメージの調査に役立てることができます。
-
上の表示においては何行かが省略(truncate)表示されているのがわかります。
--no-trunc
フラグをつければ、省略せずに表示することができます。 (--no-trunc
って英単語が省略されたフラグを使っておきながら、省略されていない結果を得ようなんて、どういうことなんでしょうねぇ。)$ docker image history --no-trunc getting-started
レイヤーのキャッシュ処理
レイヤー管理の様子をじかに見ましたので、次に重要なレッスンに進みます。 コンテナーイメージのビルド時間を減らす方法についてです。
1 つのレイヤーに変更が入ると、それ以降に続く全レイヤーは再生成されます。
利用してきた Dockerfile をもう一度見てみます...
# syntax=docker/dockerfile:1
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
image history コマンドの出力結果に戻ってみると、Dockerfile の各コマンドがイメージ内の 1 つのレイヤーに対応していることがわかります。 覚えてるでしょうか。 イメージに変更を加えたときに yarn による依存パッケージが再インストールされていました。 ではこれを変える方法ってあるんでしょうか。 ビルドのたびに同じ依存パッケージを何度も導入することになるなんて、無駄なことですよね?
これを変更するには Dockerfile の記述を組み立て直して、依存パッケージをキャッシングするようにします。
Node ベースのアプリケーションの場合、そういった依存パッケージはpackage.json
ファイルに定義されます。
そこでこのファイルのコピーを一番最初に行っておいて、その後に依存パッケージのインストールとその他ファイルのコピーを行うようにしたら、どうなるでしょう。
package.json
に対する変更があったときだけ、yarn による依存パッケージの再生成を行うようにするということです。
どうなるでしょう?
-
Dockerfile において最初に
package.json
をコピーするようにし、その後で依存パッケージのインストールとその他ファイルのコピーを行うように修正します。# syntax=docker/dockerfile:1 FROM node:12-alpine WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --production COPY . . CMD ["node", "src/index.js"]
-
Dockerfile と同じフォルダー内に
.dockerignore
という名前のファイルを生成して、その内容を以下とします。node_modules
.dockerignore
ファイルを使うと、イメージに関係するファイルのみを選別してコピーするという方法を簡単に実現できます。 この点に関しては こちら に説明しています。 今の場合node_modules
フォルダーは 2 番めのCOPY
においては処理対象からはずす必要があります。 そうしないと、RUN
におけるコマンド実行によって生成されたファイルを上書きしてしまう可能性があるからです。 Node.js アプリケーションにおいてなぜこういったことが推奨されるか、あるいはその他のベストプラクティスについて、Dockerizing a Node.js web app にガイドが示されているのでご覧ください。 -
docker build
を実行して新たなイメージをビルドします。$ docker build -t getting-started .
以下のような出力が得られます。
Sending build context to Docker daemon 219.1kB Step 1/6 : FROM node:12-alpine ---> b0dc3a5e5e9e Step 2/6 : WORKDIR /app ---> Using cache ---> 9577ae713121 Step 3/6 : COPY package.json yarn.lock ./ ---> bd5306f49fc8 Step 4/6 : RUN yarn install --production ---> Running in d53a06c9e4c2 yarn install v1.17.3 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.9: The platform "linux" is incompatible with this module. info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... [4/4] Building fresh packages... Done in 10.89s. Removing intermediate container d53a06c9e4c2 ---> 4e68fbc2d704 Step 5/6 : COPY . . ---> a239a11f68d8 Step 6/6 : CMD ["node", "src/index.js"] ---> Running in 49999f68df8f Removing intermediate container 49999f68df8f ---> e709c03bc597 Successfully built e709c03bc597 Successfully tagged getting-started:latest
すべてのレイヤーが再ビルドされたことが見てとれます。 Dockerfile を大きく変更したのですから、そうなるのも当たり前です。
-
そこで
src/static/index.html
ファイルに変更を加えます(たとえば<title>
を「The Awesome Todo App」にするとか)。 -
もう一度
docker build -t getting-started .
を実行して Docker イメージを作り直します。 今回の出力結果はやや異なります。Sending build context to Docker daemon 219.1kB Step 1/6 : FROM node:12-alpine ---> b0dc3a5e5e9e Step 2/6 : WORKDIR /app ---> Using cache ---> 9577ae713121 Step 3/6 : COPY package.json yarn.lock ./ ---> Using cache ---> bd5306f49fc8 Step 4/6 : RUN yarn install --production ---> Using cache ---> 4e68fbc2d704 Step 5/6 : COPY . . ---> cccde25a3d9a Step 6/6 : CMD ["node", "src/index.js"] ---> Running in 2be75662c150 Removing intermediate container 2be75662c150 ---> 458e5c6f080c Successfully built 458e5c6f080c Successfully tagged getting-started:latest
まずなによりもビルドが格段に速くなったことがわかります。 そして Step 1 から 4 は
Using cache
と表記され、キャッシュが利用されていることがわかります。 やりました。 ビルドキャッシュを利用することができました。 つまりこのイメージのプッシュとプル、あるいはイメージ更新がぐっと速くなるということです。 お疲れさま。
マルチステージビルド
本チュートリアルではさほど深く突き詰めていませんが、極めて強力なツールとしてマルチステージビルドがあります。 イメージの生成に複数ステージを利用するというものです。 これにはいくつかの利点があります。
- ビルド時の依存パッケージと実行時の依存パッケージを分離します。
- アプリとして実行する必要のあるもの だけ を作り出すことによって、イメージ全体のサイズを削減します。
Maven/Tomcat の例
Java ベースのアプリケーションをビルドするには、ソースコードを Java のバイトコードにコンパイルするために JDK が必要になります。 しかし JDK は本番環境では必要ありません。 またアプリのビルドに Maven や Gradle といったツールを利用するかもしれませんが、こういったものも最終イメージ内には不要のものです。 そこでマルチステージビルドが役立ちます。
# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
この例では 1 つめのステージ(build
と呼ぶ)を使って Maven を利用した Java ビルドを実現します。
2 つめのステージ(FROM tomcat
から始まるところ)において、build
ステージの生成ファイルをコピーします。
最終イメージは、この 2 つめに作り出されたステージです(これは--target
フラグを使えば上書き可能です)。
React の例
React アプリケーションをビルドするには JS コード(通常 JSX)、SASS スタイルシートなどをコンパイルしてスタティック HTML、JS、CSS を生成するために Node 環境が必要です。 サーバーにおいてレンダリングを行わないのであれば、本番ビルド用の Node 環境すら必要ありません。 スタティックリソースであるならスタティック Nginx コンテナーを使いましょう。
# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
ここで利用したnode:12
イメージは(レイヤーキャッシングの最大化を目指した上で)ビルド処理を実現するものであり、その出力結果を Nginx コンテナーにコピーしています。
すばらしい。
まとめ
イメージがどのように構成されているかについて何となく理解できたところで、イメージビルドをより速く、変更はより少なくすることができました。 イメージをスキャンしておけば、実行および配布するイメージの安全性が確実になります。 マルチステージビルドを使えば、イメージサイズ全体を小さくできることもわかりました。 ビルド時の依存パッケージを実行時の依存パッケージから切り離すことができるので、最終コンテナーの安全性が増すことになります。
get started, setup, orientation, quickstart, intro, concepts, containers, docker desktop