Dockerfileのベストプラクティスとセキュリティについて

こんにちは、主に検索周りを担当しているエンジニアの伊藤です。

この記事は 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
          

ビルドキャッシュを活用しよう

イメージをビルドするとき、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で構成されています。

f:id:pma1013:20201214095357p:plain
公式サイトから引用

  • 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 ([email protected]) ===
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 ([email protected]) ===
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
    こちらにアクセスすると、下記の画面が表示されることが確認できました。

f:id:pma1013:20201214165419p:plain

  • 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すべてにおいて脆弱性対応などしていくのは現実的ではない

セキュリティについて

私のコンテナセキュリティに対する知識としては、下記のようなレベルのものでした。

  • コンテナにおけるセキュリティって何すればいいの?
  • そもそもコンテナに限らず何をすればセキュリティちゃんとしてますって言えるの?

という訳でコンテナにおけるセキュリティ基準やツールとしてはどういったものがあるのかを調査した結果をまとめます。

概要

コンテナにおけるセキュリティ基準

コンテナの脆弱性スキャン

  • コンテナ環境もオンプレ同様にOSのライブラリやパッケージなどから構成されるため、これまで通り脆弱性対策が必須である
  • それに加えてコンテナイメージ、ランタイム環境の脆弱性にも配慮する必要がある

ツールの活用

とりあえず手軽に上記のセキュリティ基準チェックと脆弱性スキャンを行いたいというモチベーションの元、以前から気になるツールをピックアップしました。

dockle

https://github.com/goodwithtech/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には組織として統制のとれたコンテナ作成やセキュリティ基準を継続的に満たすことの手段が提供されているので、 その辺りをうまく活用していく必要性を感じでいます。

セキュリティについても検知の仕組みだけでなく、日々の運用の中でいかに対応していくかということが大事だと思うので、 今後も試行錯誤しながら少しずつ前進していければと思っています。

明日の記事の担当はインフラエンジニアの山口さんです。お楽しみに。


株式会社エニグモ 正社員の求人一覧

hrmos.co


  1. CIS(Center for Internet Security)とは、米国のNSA(National Security Agency/国家安全保障局)、DISA(Difense Informaton Systems Agency/国防情報システム局)、NIST(National Institute of Standards and Technology/米国立標準技術研究所)などの米国政府機関と、企業、学術機関などが協力して、インターネット・セキュリティ標準化に取り組む団体の名称