コンテナイメージを小さくするために
CIの時間を短くする活動を行っており, 特にその一部のアプリケーションコンテナビルドの改善について書きたいと思います.
TL;DR
- コンテナイメージを小さくするのは pull / push のコストを減らすため
- ベースイメージを小さいものにする (ex. alpine linux, distroless)
- Multi-stage Buildsを活用する
- イメージに不要なファイルが含まれていないかを確認する. dlayer
コンテナイメージを小さくするために
なぜ小さくするのか
pull / pushのコストを減らすためです. CIの時間を短くするという文脈においては
- 成果物の push
- 依存イメージの pull (docker-compose を用いたテストなど)
- docker build
が主な改善対象になります.
(1), (2) に関してはコンテナイメージを小さくすることで直接的な効果があります. とはいえマネージドなコンテナレジストリを使っていることが多いと思いますし, クラウド内の内部通信なのでそれなりに大きなイメージでない限りあまり効果はないかもしれません.
(3) が CI の中で結構な割合を占めることになると思います. docker には build のための cache 機構が存在しますが状態を保持しない CI においては使うことができません. 状態を保持しない環境においても cache を使えるように --cache-from
というオプションが存在します. --cache-from
は手元で build していないイメージを cache の対象とするためのオプションで pull と組み合わせると CI 上でも docker の build cache の機構を使うことができます.
詳しくは以下を参照ください.
- コンテナの build については Container Buildの話
- docker build cache については ちゃんと理解するdocker build cache - 薄いブログ
- cache-from については なぜ cache-from を指定しなければいけないか - 薄いブログ
どうやって小さくするか
(2018/12時点で)よくインターネット上で見られた方法として
- ベースイメージを小さいものにする
apk --no-cache
をつけたり/tmpを最後に消すようにしたり,apk add --virtual foo
を使ってコマンドの最後に削除したりする- Multi-stage Buildsを使う
- レイヤーの数を少なくする
などがありました. しかし何故小さくなるかに触れている文献を当時(2018/12)はあまり見ることができませんでした.
ベースイメージを小さくする
ベースイメージを小さくするというのは比較的簡単にできる解決策です.
ですがAlpine等の環境は慣れ親しんでいない人が多いので思わぬ落とし穴にハマりがちです, 運用しやすさなどを考慮して選択すると良いです.
不要なキャッシュやパッケージを削除する
apk --no-cache
などの環境ごとのパッケージマネージャーのキャッシュをオフにするオプションは毎回調べる, もしくは覚えることになります.
外部に公開するイメージでもない限り無視しても問題ない程度の大きさであることが多いです.
不要なパッケージの削除に関しては docker のレイヤーの仕組み上 RUN を一つにまとめないとレイヤーに残ってしまうので一つにまとめなければいけなくなります, これによる Dockerfile のメンテナンス性の低下を考えると後述の Multi-stage Buildsの活用のほうが良いと思います.
orisano/minid の様に RUN を自動でまとめるアプローチなどもありますがCIのパイプラインが複雑になりがちです.
Multi-stage Buildsを使う
Multi-stage Buildsは意外と知名度が低い問題があって(2018/12~)悲しいです.
これを使うことでイメージ(ステージ)間のファイルのCOPYが可能になり, ビルドに必要な依存関係と実行に必要な依存関係の分離が実現されます.
ビルドに必要な依存関係と実行に必要な依存関係が大きく異なる場合, 最終イメージを小さくすることにかなり寄与します.
しかしCIの時間を短くするためのdocker build cacheの観点で見ると最終ステージ以外も小さくしないといけないので本質な問題は解決しません.
レイヤーの数を少なくする
(2018/12時点で)これは少し前まで結構見た言説ですがこれは本当なんでしょうか. (2020/7)正しくないわけではないが Multi-stage Buildsを使ったほうが良いです.
なぜ小さくなるのか
コンテナイメージが大きい原因調査のためにdocker historyを使って説明しているものがありました.
RUNを2つ書いたものとRUNを1つにまとめたものの docker history の結果を比較して小さくなっていることを確認したり, パッケージの install をしている部分が大きくなっているので cache が作られているのではないかという仮説を立てたりしているものでした.
(2018/12時点で)あまりコンテナイメージのサイズに興味がない人が多いので原因を突き詰めないのは仕方のないことかもしれません.
docker historyは非常に有用なツールですがどのレイヤーが重そうかのあたりをつけるくらいにしか使えません. なぜ重いのかを掘り下げることができません.
そもそもコンテナイメージがどの様に保持されているかを知っているとなぜ重いかの理解が捗ります.
moby/v1.md at master · moby/moby · GitHub
イメージの仕様を見てみるとコンテナイメージは差分により構成されていることがわかります. 以下のページの図が理解しやすいです.
どこの差分がなぜ重くなっているか知ることでより本質的な改善が行えるのではないかと考え, ツールを作りました.
docker saveで生成されるファイルを分析して, レイヤーごとに差分のサイズとファイル名が出力するツールです.
不明なcacheファイルが残っていることや, 不要なツール, 重複して作られているファイルの存在の認識ができコンテナイメージを小さくする際に力を発揮します.
実際にGoの公式イメージサイズを小さくすることに成功しました. 改善活動をするときは原因特定ができる状況を作りましょう.
ぜひdlayerを使ってコンテナイメージを小さくしてみてください!
余談
このツールが出た少しあとに以下のツールが爆発的に盛り上がりました.