こちらは、2020年LCLアドベントカレンダーの1日目です!
こんにちは。最近は作業用BGMがもっぱらバーチャル猫さんの曲になっている id:kasei_san です
今回は、LCLで実際に使用している LinterとセキュリティチェッカーをつかったモダンなDockerfileの書き方と、それらを利用して作成したDockerfileを紹介したいと思います!
みなさんはベストプラクティスに基づいたDockerfile書けていますか?
Dockerのベストプラクティスは、公式でも記事になっていますが、全てを理解して書くのは中々難しいですよね...
そんな時のために、自動的にDockerfileがベストプラクティスに則っているかチェックしてくれるLinterがあります!
それが、hadolintです
hadolintはDockerfileがベストプラクティスに沿っているかチェックして、おおまかな修正方法まで教えてくれます。便利!
hadolintのつかいかた
macOSであれば、brewでインストールできます
brew install hadolint
そして、以下のコマンドでDockerfileをチェックしてくれます
hadolint ${Dockerfileのpath}
例えば、Dockerfileに以下のようなコマンドがあった場合...
COPY Gemfile Gemfile.lock package.json yarn.lock app
ベストプラクティス DL3021 に反していると警告してくれます
Dockerfile:52 DL3021 COPY with more than 2 arguments requires the last argument to end with /
なお、ベストプラクティスは、hadolintのgithubのwikiにまとまっていて、そちらに具体的な解決方法も記されています。親切!
Home · hadolint/hadolint Wiki · GitHub
Dockerfileの書き方はいろいろなベストプラクティスがあり、自力で覚えていくのは大変ですが、こういったLinterを使うことで、素早く/自動的に身体に叩き込むことができますね!
これで、ベストプラクティスに沿ったDockerfileを書くことができました! しかし、ビルドされたImage。本当にセキュアでしょうか...?
セキュアなDockerfileを書けていますか?
と、いうわけで次にdockleを紹介します
dockleはビルドしたDockerImageをスキャンして、セキュリティ上の問題が無いかチェックしてくれるツールです
dockleのつかいかた
READMEに書いてあるとおりですね
$ brew untap goodwithtech/dockle # who use 0.1.16 or older version $ brew install goodwithtech/r/dockle $ dockle [YOUR_IMAGE_NAME]
実行してみると、例えば root ユーザで実行しようとしていると以下のような警告が出ます
WARN - CIS-DI-0001: Create a user for the container * Last user should not be root
CIS-DI-0001
などのidで分類されているチェックポイントの詳細は以下で知ることができます
dockle/CHECKPOINT.md at master · goodwithtech/dockle · GitHub
すべてを理解するのはなかなか難しいセキュリティをしっかり抑えて教えてくれるので、 hadlintと同じく何度もつかっていくうちに、勘所がだんだん覚えられてきてとても良いです...!
できあがったモダンでセキュアなDockerfileを紹介
最後にdockleとhadlintをつかって作成した、Railsを動作させる本番用のDockerfileを紹介します
意図が分かりづらい部分には、チェックポイントのリンクをコメントしていますので、たどってみると参考になると思います!
################################################################ # rails Image ################################################################ FROM ruby:2.7.2-slim ENV RAILS_ENV production ENV ROOT_PATH /app ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 # パイプ(|) を使用した場合、パイプ前の処理が失敗しても # 最後の処理が成功すれば、その行のexitは0になる # pipefail オプションをつけることで、パイプの前の処理が失敗した場合も # 戻り値を非1とすることが可能 # # see: https://github.com/hadolint/hadolint/wiki/DL4006 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # 特殊なアクセス権、suid, sgid を排除 # 想定外のアクセス権でコマンドを実行されることを防ぐ # # see: https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#CIS-DI-0008 RUN chmod u-s /bin/umount && \ chmod g-s /usr/bin/expiry && \ chmod u-s /bin/su && \ chmod g-s /usr/bin/wall && \ chmod u-s /usr/bin/gpasswd && \ chmod u-s /bin/mount && \ chmod u-s /usr/bin/passwd && \ chmod g-s /sbin/unix_chkpwd && \ chmod g-s /usr/bin/chage && \ chmod u-s /usr/bin/chsh && \ chmod u-s /usr/bin/chfn && \ chmod u-s /usr/bin/newgrp RUN apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ build-essential=12.6 \ gnupg=2.2.12-1+deb10u1 \ wget=1.20.1-1.1 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ truncate -s 0 /var/log/*log # pg をインストールするためにpostgresqlが必要 ARG PG_MAGER_VERSION=11 ARG PG_VERSION=11.10-1.pgdg100+1 RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main" $PG_MAGER_VERSION > /etc/apt/sources.list.d/pgdg.list && \ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ "postgresql-server-dev-$PG_MAGER_VERSION=$PG_VERSION" && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ truncate -s 0 /var/log/*log ENV GEM_HOME=/bundle \ BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 \ BUNDLE_PATH=${GEM_HOME} \ BUNDLE_BIN=${GEM_HOME}/bin \ BUNDLE_APP_CONFIG=${GEM_HOME} ARG BUNDLER_VERSION=2.1.4 RUN gem install "bundler:$BUNDLER_VERSION" ENV PATH=/app/bin:$BUNDLE_BIN:$PATH # 作業ディレクトリ & 実行ユーザ作成 RUN useradd -m -u 1000 rails && \ mkdir $ROOT_PATH && \ chown rails $ROOT_PATH USER rails WORKDIR $ROOT_PATH # 全部コピーするとRailsアプリの変更の度にここからビルドし直しになるので # 必要なGemfileやyarnのファイルを先にコピーする COPY --chown=rails Gemfile Gemfile.lock $ROOT_PATH/ # bundle installの実行 RUN bundle config set without 'development test' && \ bundle install -j4 # アプリのコピー COPY --chown=rails . $ROOT_PATH # HEALTHCHECK が失敗した場合は処理が終了する # # see: https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#cis-di-0006 HEALTHCHECK --interval=5m --timeout=30s \ CMD curl -f http://localhost:10080/robots.txt || exit 1 # 起動 CMD ["/bin/bash", "-c", "echo 'コマンドを指定してください'"]
Dockerfileの解説
アプリサーバとバッチで同様のImageを使用するため、 CMD
では何も実行しないようになっています
また、以下のdockleのチェックポイントは、対応不可能のため無効化しています
- CIS-DI-0005:
DOCKER_CONTENT_TRUST
は、ECRではサポートされていないため無効化しています - DKL-LI-0003: ImageにDockerfileが含まれているという警告が出ますが、gem等に入っているDockerfileについても警告が出てしまうため無効化しています
まとめと感想
hadlintとdockleを使うことで、モダンでセキュアなDockerfileを作れるようになりました!
先程も書きましたが、やはりベストプラクティスやセキュリティについて、ツールがサポートしてくれることで開発者が学習していけるのはとても良いですね
ただ、見ての通りDockerfileはかなり複雑になってしまいます...。おそらくDocker初心者が見たら何をしているのか把握するのはとても大変なのではないでしょうか...
また、aptでインストールするアプリケーションのバージョンも固定化されるため、これらのアプリケーションのバージョンアップにも目を光らせる必要があります (こちらは、コンテナの脆弱性スキャンツールのClairやTrivyで対応する予定です)
EC2上で直接アプリを動かすことと比べると、だいぶ楽になったとはおもいますが、それでもベストプラクティスに基づいてセキュアなアプリを動かし続けることは中々大変です
現在のLCLの規模では、そのあたりをしっかりサポートしていくのはなかなか難しいため、AWS lambda のようなよりサーバレスなアーキテクチャに移行していくのがベストかも知れませんね...!
以上です!
明日は、naotsu-logさんの Nuxt.jsで始める省エネフロントエンド開発 です! 楽しみですね!
🎄 それでは皆さん。良いクリスマスを! 🎅