dockerに入門してみる(その9)(終)

前回の記事はこちら↓

ren-opdev.hatenablog.com

今回は"dockerに入門してみる"シリーズの最終回です。最後は、コンテナイメージをビルドする上でのベストプラクティスを見ていきます。

イメージをスキャンする

事前に見つけられる脆弱性があるならば、見つけて潰してしまいたいですよね。

DockerはSnykとパートナーになっているため、docker scanによってコンテナイメージの脆弱性をスキャンすることができます。
試しにtutorialを動かしているgetting-startedをスキャンしてみます。

PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker scan getting-started
Docker Scan relies upon access to Snyk, a third party provider, do you consent to proceed using Snyk? (y/N)
y

Testing getting-started...

✗ Medium severity vulnerability found in openssl/libcrypto1.1
  Description: NULL Pointer Dereference
  Info: https://snyk.io/vuln/SNYK-ALPINE311-OPENSSL-1051931
  Introduced through: openssl/libcrypto1.1@1.1.1g-r0, openssl/libssl1.1@1.1.1g-r0, apk-tools/apk-tools@2.10.5-r0, libtls-standalone/libtls-standalone@2.9.1-r0
  From: openssl/libcrypto1.1@1.1.1g-r0
  From: openssl/libssl1.1@1.1.1g-r0 > openssl/libcrypto1.1@1.1.1g-r0
  From: apk-tools/apk-tools@2.10.5-r0 > openssl/libcrypto1.1@1.1.1g-r0
  and 4 more...
  Fixed in: 1.1.1i-r0

✗ Medium severity vulnerability found in musl/musl
  Description: Out-of-bounds Write
  Info: https://snyk.io/vuln/SNYK-ALPINE311-MUSL-1042763
  Introduced through: musl/musl@1.1.24-r2, busybox/busybox@1.31.1-r9, alpine-baselayout/alpine-baselayout@3.2.0-r3, openssl/libcrypto1.1@1.1.1g-r0, openssl/libssl1.1@1.1.1g-r0, zlib/zlib@1.2.11-r3, apk-tools/apk-tools@2.10.5-r0, libtls-standalone/libtls-standalone@2.9.1-r0, busybox/ssl_client@1.31.1-r9, gcc/libgcc@9.3.0-r0, musl/musl-utils@1.1.24-r2, pax-utils/scanelf@1.2.4-r0, libc-dev/libc-utils@0.7.2-r0
  From: musl/musl@1.1.24-r2
  From: busybox/busybox@1.31.1-r9 > musl/musl@1.1.24-r2
  From: alpine-baselayout/alpine-baselayout@3.2.0-r3 > musl/musl@1.1.24-r2
  and 12 more...
  Fixed in: 1.1.24-r3



Organization:      undefined
Package manager:   apk
Project name:      docker-image|getting-started
Docker image:      getting-started
Platform:          linux/amd64

Tested 16 dependencies for known vulnerabilities, found 2 vulnerabilities.

For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp

スキャン結果として、見つかった脆弱性の種類やどのバージョンのライブラリで修正されているかを吐き出してくれます。docker scanのオプションについては、下記の公式に詳しく載っています。 docs.docker.com コマンドラインからイメージをスキャンするほか、Docker Hubごとスキャンすることも出来るようです。 docs.docker.com

イメージ構成を見る

docker image historyコマンドを用いると、そのイメージがどのレイヤーで構成されているかを見ることができます。

PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker image history getting-started
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
7d2a760d3274   13 days ago    CMD ["node" "src/index.js"]                     0B        buildkit.dockerfile.v0
<missing>      13 days ago    RUN /bin/sh -c yarn install --production # b…   85.2MB    buildkit.dockerfile.v0
<missing>      13 days ago    COPY . . # buildkit                             4.62MB    buildkit.dockerfile.v0
<missing>      2 weeks ago    WORKDIR /app                                    0B        buildkit.dockerfile.v0
<missing>      5 weeks ago    /bin/sh -c #(nop)  CMD ["node"]                 0B
<missing>      5 weeks ago    /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B
<missing>      5 weeks ago    /bin/sh -c #(nop) COPY file:238737301d473041…   116B
<missing>      5 weeks ago    /bin/sh -c apk add --no-cache --virtual .bui…   7.62MB
<missing>      5 weeks ago    /bin/sh -c #(nop)  ENV YARN_VERSION=1.22.5      0B
<missing>      5 weeks ago    /bin/sh -c addgroup -g 1000 node     && addu…   76.5MB
<missing>      5 weeks ago    /bin/sh -c #(nop)  ENV NODE_VERSION=12.20.0     0B
<missing>      8 months ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      8 months ago   /bin/sh -c #(nop) ADD file:b91adb67b670d3a6f…   5.61MB

出力された各行がレイヤーを表しています。手軽にレイヤーのサイズを見ることができるので、どのレイヤーが肥大化しているかをパッと判別することができます。

レイヤーキャッシュを用いてビルド回数を減らす

Dockerfileの書き方を工夫することで、ビルド回数を減らすことができます。まずは、以前のチュートリアルで触ったDockerfileを引用します。

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

docker image historyで見た通り、コマンドそれぞれは一つずつレイヤーになっています。また、イメージに変更を加えるとyarnの依存関係を再インストールする必要があります。
ビルドするたびに同じ依存関係をインストールし直していくのは非効率なので、上手いこと依存関係をキャッシュして解決していきます。

Nodeベースのアプリケーションでは、アプリケーションの依存関係は全てpackage.jsonに記述されます。そのため、まずpackage.jsonをコピーして依存関係をインストールしてしまい、その後に他のソースコードをコピーすれば余計な再インストールを避けられます。

具体的には、以下のようにDockerfileを書きかえます。

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はDocker(正確にはビルド時に動作するdaemon)に無視してほしいファイルを指定するためのファイルで、今回node_modulesはRUNコマンドを実行した際に上書きされるため無視する対象とします。

では、一度イメージをビルドしてみます。

PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker build -t getting-started .
[+] Building 13.9s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.1s
 => => transferring dockerfile: 175B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 52B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/node:12-alpine                                                  0.0s
 => [1/5] FROM docker.io/library/node:12-alpine                                                                    0.2s
 => => resolve docker.io/library/node:12-alpine                                                                    0.0s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 8.65kB                                                                                0.0s
 => [2/5] WORKDIR /app                                                                                             0.0s
 => [3/5] COPY package.json yarn.lock ./                                                                           0.1s
 => [4/5] RUN yarn install --production                                                                           12.1s
 => [5/5] COPY . .                                                                                                 0.1s
 => exporting to image                                                                                             1.3s
 => => exporting layers                                                                                            1.3s
 => => writing image sha256:d71dd886d28378420a1fb3f6782c4d9816f52e4761d08ecca02ca3c893818ebc                       0.0s
 => => naming to docker.io/library/getting-started

Dockerfileの記述通りに処理が進んでいますね。
では次に、src/static/index.htmlを適当にいじってみて再度ビルドしてみます。

PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker build -t getting-started .
[+] Building 0.3s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 32B                                                                                0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 34B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/node:12-alpine                                                  0.0s
 => [1/5] FROM docker.io/library/node:12-alpine                                                                    0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 3.43kB                                                                                0.0s
 => CACHED [2/5] WORKDIR /app                                                                                      0.0s
 => CACHED [3/5] COPY package.json yarn.lock ./                                                                    0.0s
 => CACHED [4/5] RUN yarn install --production                                                                     0.0s
 => [5/5] COPY . .                                                                                                 0.1s
 => exporting to image                                                                                             0.1s
 => => exporting layers                                                                                            0.1s
 => => writing image sha256:2807d4c8a4f026a88ac4005c791d060de6a8a23ae1275046047b1b08499688c3                       0.0s
 => => naming to docker.io/library/getting-started

途中CACHEDとある通り、イメージのキャッシュが上手く働いて余計なインストールを省けたことが分かります。これで、より効率的なビルド作業が実現できますね!

マルチステージビルド

ビルドするだけのステージと、成果物を載せるだけのステージ…と複数のステージを使い分けていくことで、最終的なイメージのサイズを小さくすることができます。

Maven/Tomcatの例

Javaベースのアプリケーションをビルドするには、コンパイル時にJDKが必要となります。しかし、JDK自体はプロダクション環境には必要ないものです。MavenやGradleを使用する際も同様ですね。こういう場合に、マルチステージビルドが有用となります。

FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

この例では、buildステージでMavenを使用したJavaアプリケーションのビルドを行い、tomcatステージでbuildステージからビルド成果物をコピーしています。

Reactの例

Reactアプリケーションをビルドするには、Node環境が必要となります。しかし、サーバーサイドで画面の描画を行わない限りはプロダクションビルドにNode環境は必要ありません。こういう場合にも、マルチステージビルドが有用です。

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コンテナにコピーするだけ…。Nodeまわりのことはよく分かりませんが、確かに効率的っぽいですね。


コンテナイメージのビルドをより速く、より効率的に行う方法を(少し)知ったところで、このチャプターは終わりです。そして、これにてDocker 101 Tutorialも終了です!

Dockerの世界はまだまだ広く、一口にDockerといってもコンテナオーケストレーションCNCFなど様々な領域があります。 landscape.cncf.io 会社ではKubernetesを利用しているため、次は話題のイラストでわかるDockerとKubernetesを読んで更にDocker自体の理解やKubernetesへの理解を深めたいです。

とりあえずは、このチュートリアルを年内に終えられてよかったです。
よいお年をお迎えください!