こんにちは、主に検索周りを担当しているエンジニアの伊藤です。
この記事は Enigmo Advent Calendar 2020 の 17 日目の記事です。
みなさんは適切なDockerfileを書けていますか?とりあえずイメージのビルドが出来ればいいやとなっていませんか? 今回は自戒の意味も込めて、改めてDockefileのベストプラクティスについて触れつつ、 そもそもDockerfileを書かずにコンテナイメージをビルドする方法とコンテナセキュリティに関する内容についてまとめてみました。
Dockerfileのベストプラクティス
ご存知の方も多いと思いますが、こちらがDocker社が推奨するベストプラクティとなっています。 せっかくなので事例を交えていくつかピックアップしてみます。
イメージサイズは極力小さくしよう
- 軽量なベースイメージを選択する
- Docker社の推奨はdebian
- 不要なパッケージはインストールしない
- レイヤはなるべく減らす
- RUN/COPY/ADDだけがレイヤを増やすのでこれを使用するときに意識しましょう。
- RUNで実行するコマンドは極力
&&
で連結する - 可能な場合はマルチステージビルドを利用する
- ADDを使用したアンチパターン(下記の例ではADDによって圧縮ファイルを含んだレイヤが余計に作成されてしまう)
-
ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all
推奨例
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz \ | tar -xJC /usr/src/things \ && make -C /usr/src/things all
-
- RUNで実行するコマンドは極力
- RUN/COPY/ADDだけがレイヤを増やすのでこれを使用するときに意識しましょう。
ビルドキャッシュを活用しよう
イメージをビルドするとき、DockerはDockerfileに書かれた命令を上から順番に実施します。
その際、各命令毎にキャッシュ内で再利用できる既存のイメージを探しますが、なければ以降のキャッシュは破棄されます。
そのため、更新頻度が高いものをDockerfileの後ろの方に記載することが重要になります。
例えば下記はapp
というアプリケーションコードを含むディレクトリをコンテナにコピーし、
pip installによって必要なライブラリをインストールする例です。
COPY app /tmp/ RUN pip install --requirement /tmp/requirements.txt
- 推奨例
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY app /tmp/
一見すると前者の方がレイヤが少ない分、良さそうに見えますが、
app
配下のコードに変更が入るたびにライブラリのインストールも行われ、
その分ビルド時間が伸びてしまいます。
Dockerfileに関する悩みどころ
ここまでDockerfileに関するベストプラクティスについて触れてきましたが、Dockerfileを作成、メンテするのって大変ではないですか?
- どのベースイメージを使用すべきか?
- イメージサイズが大きくなりすぎる
- イメージサイズの削減を頑張ってたら時間が溶けた(開発作業に専念したいのに。。。)
- Dockerfile自体のメンテが辛い
- イメージサイズを小さくしようと思うとDockerfile自体の可読性が下がるというつらみ
- ベストプラクティスを意識することが自体が辛い
- セキュリティ的な懸念
- 使用するベースイメージに脆弱性が含まれていないかなど
Dockerfileを書かないという選択肢
そこで続いてのお話がBuildpackについてです。 こちらを利用することでDockerfileを書くことなく、ソースコードからコンテナイメージを生成することが可能になるというものです。
Buildpack
- 2011年にHerokuが考案し、
Cloud Foundry
、Gitlab
、Knative
等で採用されている仕組み- 例えばGitlabではAuto DevOps(Auto Build)で利用されています
- 様々な言語のBuildpackを使ってユーザのアプリケーションコードに対して、「判定」、「ビルド」、「イメージ化」といった一連の流れを実施する事によって、基盤上で動作可能な形にアプリケーションコードを組み立てる
Cloud Native Buildpacks
- 上記のHerokuオリジナルと呼ばれるBuildpackが特定の実行基盤でしか動作しないというでデメリットがあったのに対し、Dockerの急速な普及を背景に、OCIイメージのようなコンテナ標準を採用したイメージを作成しようと始まったのがCloud Native Buildpacks(以降 CNBと略) Projectです。
- HerokuとPivotalが中心となって2018年1月にCNCF傘下でスタートし、現時点でCNCFのSandboxプロジェクトという立ち位置になっています
- 以降はこちらのCNBについての概要について記載します
CNBの仕組み
CNBを利用してイメージを生成する際はビルダー
というものを指定します。
ビルダーはアプリのビルド方法に関するすべての部品と情報をバンドルしたイメージとなっており、複数のbuildpack
、lifecycle
、stack
で構成されています。
- buildpack
- ソースコードを検査し、アプリケーションをどうビルドし実行するかを決める
- lifecycle
- buildpackの実行を調整し、最終的なイメージを組み立てる
- stack
- ビルド及び実行環境用のコンテナイメージのペア
デモ
基本的にCNBを利用して運用していく際には、自前のビルダーを作成することになると思います。 今回はお試しということで、すでにあるビルダーを使って試してみたいと思います。
- 前提条件
- ローカル環境に
Docker
及びBuildpack
がインストール済みであること
- ローカル環境に
- サンプルコード
- Flaskを利用したWebアプリケーション(単純にHello Worldと出力するだけのもの)
- 構成としては下記の通りで最低限のファイルのみ配置しています。
. ├── requirements.txt └── src ├── __init__.py ├── app.py └── templates └── index.html
- ビルド
それではpackコマンドを使ってビルドしてみましょう。
$ pack build sample-cnb:0.0.1 Please select a default builder with: pack set-default-builder <builder-image> Suggested builders: Google: gcr.io/buildpacks/builder:v1 Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python Heroku: heroku/buildpacks:18 heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP Paketo Buildpacks: paketobuildpacks/builder:base Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, Ruby, NGINX and Procfile Paketo Buildpacks: paketobuildpacks/builder:full Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, PHP, Ruby, Apache HTTPD, NGINX and Procfile Paketo Buildpacks: paketobuildpacks/builder:tiny Tiny base image (bionic build image, distroless-like run image) with buildpacks for Java Native Image and Go Tip: Learn more about a specific builder with: pack inspect-builder <builder-image>
packコマンドを実行すると上記のようにビルダーを指定しろと言われます。 今回はここでおすすめされている Google Cloud Buildpacks を利用して実行します。
$ pack build sample-cnb:0.0.1 --builder gcr.io/buildpacks/builder:v1 v1: Pulling from buildpacks/builder Digest: sha256:f0bb866219220921cbc094ca7ac2baf7ee4a7f32ed965ed2d5e2abbf20e2b255 Status: Image is up to date for gcr.io/buildpacks/builder:v1 v1: Pulling from buildpacks/gcp/run Digest: sha256:83eb67ec38bb38c275d732b07775231e7289e0e2b076b12d5567a0c401873eb7 Status: Image is up to date for gcr.io/buildpacks/gcp/run:v1 ===> DETECTING google.python.runtime 0.9.1 google.python.missing-entrypoint 0.9.0 google.utils.label 0.0.1 ===> ANALYZING Previous image with name "sample-cnb:0.0.1" not found ===> RESTORING ===> BUILDING === Python - Runtime (google.python.runtime@0.9.1) === Using runtime version from .python-version: 3.7.8 Installing Python v3.7.8 Upgrading pip to the latest version and installing build tools -------------------------------------------------------------------------------- Running "/layers/google.python.runtime/python/bin/python3 -m pip install --upgrade pip setuptools wheel" Collecting pip Downloading pip-20.3.1-py2.py3-none-any.whl (1.5 MB) Collecting setuptools Downloading setuptools-51.0.0-py3-none-any.whl (785 kB) Collecting wheel Downloading wheel-0.36.2-py2.py3-none-any.whl (35 kB) Installing collected packages: pip, setuptools, wheel Attempting uninstall: pip Found existing installation: pip 20.1.1 Uninstalling pip-20.1.1: Successfully uninstalled pip-20.1.1 Attempting uninstall: setuptools Found existing installation: setuptools 47.1.0 Uninstalling setuptools-47.1.0: Successfully uninstalled setuptools-47.1.0 Successfully installed pip-20.3.1 setuptools-51.0.0 wheel-0.36.2 Done "/layers/google.python.runtime/python/bin/python3 -m pip inst..." (6.427479028s) === Python - pip (google.python.missing-entrypoint@0.9.0) === Failure: (ID: 194879d1) Failed to run /bin/build: for Python, an entrypoint must be manually set, either with "GOOGLE_ENTRYPOINT" env var or by creating a "Procfile" file -------------------------------------------------------------------------------- Sorry your project couldn't be built. Our documentation explains ways to configure Buildpacks to better recognise your project: -> https://github.com/GoogleCloudPlatform/buildpacks/blob/main/README.md If you think you've found an issue, please report it: -> https://github.com/GoogleCloudPlatform/buildpacks/issues/new -------------------------------------------------------------------------------- ERROR: failed to build: exit status 1 ERROR: failed to build: executing lifecycle: failed with status code: 145
今度は上記のようなエラーが出力されます。
どうやらDockerfileのentrypointに相当する GOOGLE_ENTRYPOINT
を設定する必要があるようです。
該当のオプションを追加して下記の通り再トライしてみます。
$ pack build sample-cnb:0.0.1 --builder gcr.io/buildpacks/builder:v1 --env GOOGLE_ENTRYPOINT="flask run --host 0.0.0.0 --port 5000" 〜省略〜 Adding cache layer 'google.python.pip:pip' Adding cache layer 'google.python.pip:pipcache' Successfully built image sample-cnb:0.0.1
上記のようにSuccessfully
と出力されれば無事にコンテナイメージのビルドは完了しています。
作成されたイメージを確認してみましょう。
REPOSITORY TAG IMAGE ID CREATED SIZE sample-cnb 0.0.1 4c60a192da62 40 years ago 289MB
sample-cnb
というイメージが作成されていることが確認できました。
ここで気になるのは作成日が40 years ago
となっていることです。
これについては公式サイトに記載がありましたが、
どうやら再現可能なビルドを目的とした意図的な設計のようです。
- コンテナ起動
ビルドしたコンテナを起動して正常に動作することを確認します。 下記コマンドでコンテナを起動して、
$ docker run --rm -p 5000:5000 -e FLASK_ENV=development sample-cnb:0.0.1
こちらにアクセスすると、下記の画面が表示されることが確認できました。
Dockerfileを使ったビルド
最後に比較のためにDockerfileを利用したビルドも行います。- Dockerfileの準備
FROM python:3.7 WORKDIR /app COPY requirements.txt /app RUN pip install -r requirements.txt COPY src /app/ ENV FLASK_APP=/app/app.py ENTRYPOINT ["flask", "run"] CMD ["--host", "0.0.0.0", "--port", "5000"]
ビルド
$ docker build -t sample-df:0.0.1 .
比較
Dockerfileベースでビルドしたイメージは下記の通りとなります。 CNBで作成したイメージの方が軽量なOSが利用されていることが分かります
REPOSITORY TAG IMAGE ID CREATED SIZE sample-df 0.0.1 9a5c14fd1846 14 seconds ago 928MB
CNBのメリット
CNBのメリットをざっとまとめると下記のような感じになるかと思います。
- 開発に注力できる
- 開発者はDockerfileを作成、メンテすることから開放される
- 持続可能な運用
- スケーラブルなセキュリティ対応
- 散在しがちなDockerfileすべてにおいて脆弱性対応などしていくのは現実的ではない
- スケーラブルなセキュリティ対応
セキュリティについて
私のコンテナセキュリティに対する知識としては、下記のようなレベルのものでした。
- コンテナにおけるセキュリティって何すればいいの?
- そもそもコンテナに限らず何をすればセキュリティちゃんとしてますって言えるの?
という訳でコンテナにおけるセキュリティ基準やツールとしてはどういったものがあるのかを調査した結果をまとめます。
概要
コンテナにおけるセキュリティ基準
コンテナの脆弱性スキャン
ツールの活用
とりあえず手軽に上記のセキュリティ基準チェックと脆弱性スキャンを行いたいというモチベーションの元、以前から気になるツールをピックアップしました。
dockle
https://github.com/goodwithtech/dockle
概要
- CIS Benchmarkに対応
- ベストプラクティスのチェック
使い方
dockle [イメージ名]
trivy
https://github.com/aquasecurity/trivy
- 概要
- コンテナの脆弱性スキャンツール
- 使い方
trivy [イメージ名]
デモ
ここで上記で作成したコンテナイメージ(Dockerfileから作成したイメージとCNBで作成したイメージ)をそれぞれのツールにかけた場合にどういった結果になるか確認してみたいと思います。
Dockerfileベース
まずはDockerfileからビルドしたイメージの方です。
- dockle
- WARNレベルが1件検知されました。
$ dockle sample-df:0.0.1 WARN - CIS-DI-0001: Create a user for the container * Last user should not be root INFO - CIS-DI-0005: Enable Content trust for Docker * export DOCKER_CONTENT_TRUST=1 before docker pull/build INFO - CIS-DI-0006: Add HEALTHCHECK instruction to the container image * not found HEALTHCHECK statement INFO - CIS-DI-0008: Confirm safety of setuid/setgid files * setuid file: usr/bin/chfn urwxr-xr-x * setgid file: usr/bin/ssh-agent grwxr-xr-x * setuid file: usr/lib/openssh/ssh-keysign urwxr-xr-x * setuid file: bin/umount urwxr-xr-x * setgid file: usr/bin/wall grwxr-xr-x * setuid file: bin/mount urwxr-xr-x * setuid file: usr/bin/gpasswd urwxr-xr-x * setuid file: usr/bin/passwd urwxr-xr-x * setgid file: usr/bin/chage grwxr-xr-x * setuid file: bin/su urwxr-xr-x * setuid file: bin/ping urwxr-xr-x * setgid file: usr/bin/expiry grwxr-xr-x * setuid file: usr/bin/newgrp urwxr-xr-x * setuid file: usr/bin/chsh urwxr-xr-x * setgid file: sbin/unix_chkpwd grwxr-xr-x
- trivy
- こちらは大量の出力結果が表示されるためサマリのみ貼っておきます。 CRITICALなものが69件検知されていることが分かります。
$ trivy sample-df:0.0.1 sample-df:0.0.1 (debian 10.2) ============================= Total: 2401 (UNKNOWN: 23, LOW: 1291, MEDIUM: 520, HIGH: 498, CRITICAL: 69)
CNBベース
続いてCNBでビルドしたイメージの方を確認してみます。
- dockle
- こちらはWARNレベルのものは1件もなく、INFOレベルのものだけが検知されました。
$ dockle sample-cnb:0.0.1 INFO - CIS-DI-0005: Enable Content trust for Docker * export DOCKER_CONTENT_TRUST=1 before docker pull/build INFO - CIS-DI-0006: Add HEALTHCHECK instruction to the container image * not found HEALTHCHECK statement INFO - CIS-DI-0008: Confirm safety of setuid/setgid files * setgid file: usr/bin/expiry grwxr-xr-x * setuid file: bin/umount urwxr-xr-x * setgid file: usr/bin/chage grwxr-xr-x * setuid file: usr/bin/newgrp urwxr-xr-x * setgid file: usr/bin/wall grwxr-xr-x * setuid file: usr/bin/chsh urwxr-xr-x * setuid file: bin/su urwxr-xr-x * setuid file: usr/bin/passwd urwxr-xr-x * setuid file: usr/bin/gpasswd urwxr-xr-x * setuid file: usr/bin/chfn urwxr-xr-x * setuid file: bin/mount urwxr-xr-x * setgid file: sbin/unix_chkpwd grwxr-xr-x * setgid file: sbin/pam_extrausers_chkpwd grwxr-xr-x
- trivy
- こちらもサマリのみ貼りますが、CRITICALに関しては0件となっています
$ trivy sample-cnb:0.0.1 2020-12-14T19:22:18.244+0900 INFO Detecting Ubuntu vulnerabilities... sample-cnb:0.0.1 (ubuntu 18.04) =============================== Total: 75 (UNKNOWN: 0, LOW: 53, MEDIUM: 20, HIGH: 2, CRITICAL: 0)
この結果からもGoogle Cloud Buidpackを利用してビルドしたイメージの方が軽量かつセキュアな環境であることが分かると思います。
CIへの組み込み
上で紹介したツールはいずれもCIに組み込んで使用することも想定して作られています。 下記のようにオプションを指定して使うことで、CIのタイミングで実行&確認がしやすくなっています。
- dockle
dockle --exit-code 1 [イメージ名]
- trivy
trivy --exit-code 1 --severity CRITICAL --no-progress [イメージ名]
まとめ
今回はDockerfileのベストプラクティスのおさらいと、CNBを利用したコンテナイメージのビルド方法、セキュリティに関してさらっとまとめてみました。
今後もコンテナベースのアプリケーション開発が進むと、 これまで個人、チームレベルで任せていたDockerfileの作成、管理が破綻するのではと感じました。 CNBには組織として統制のとれたコンテナ作成やセキュリティ基準を継続的に満たすことの手段が提供されているので、 その辺りをうまく活用していく必要性を感じでいます。
セキュリティについても検知の仕組みだけでなく、日々の運用の中でいかに対応していくかということが大事だと思うので、 今後も試行錯誤しながら少しずつ前進していければと思っています。
明日の記事の担当はインフラエンジニアの山口さんです。お楽しみに。
株式会社エニグモ 正社員の求人一覧