薄いブログ

技術の雑多なことを書く場所

コンテナイメージを小さくするために

CIの時間を短くする活動を行っており, 特にその一部のアプリケーションコンテナビルドの改善について書きたいと思います.

TL;DR

  • コンテナイメージを小さくするのは pull / push のコストを減らすため
  • ベースイメージを小さいものにする (ex. alpine linux, distroless)
  • Multi-stage Buildsを活用する
  • イメージに不要なファイルが含まれていないかを確認する. dlayer

コンテナイメージを小さくするために

なぜ小さくするのか

pull / pushのコストを減らすためです. CIの時間を短くするという文脈においては

  1. 成果物の push
  2. 依存イメージの pull (docker-compose を用いたテストなど)
  3. docker build

が主な改善対象になります.

(1), (2) に関してはコンテナイメージを小さくすることで直接的な効果があります. とはいえマネージドなコンテナレジストリを使っていることが多いと思いますし, クラウド内の内部通信なのでそれなりに大きなイメージでない限りあまり効果はないかもしれません.

(3) が CI の中で結構な割合を占めることになると思います. docker には build のための cache 機構が存在しますが状態を保持しない CI においては使うことができません. 状態を保持しない環境においても cache を使えるように --cache-from というオプションが存在します. --cache-from は手元で build していないイメージを cache の対象とするためのオプションで pull と組み合わせると CI 上でも docker の build cache の機構を使うことができます.

詳しくは以下を参照ください.

どうやって小さくするか

(2018/12時点で)よくインターネット上で見られた方法として

  • ベースイメージを小さいものにする
    • Alpine Linux
      • Alpine Linuxベースのイメージが提供されている場合も多いですが, glibc ではなく musl なので glibc でしか動かないソフトウェアや musl が嫌いな人間は使わない感じになっています.
    • Distroless
      • こっちはglibcなどをgoogleがメンテナンスしてくれる軽量の実行環境のみが存在するイメージです. 基本的にタグは打たれずlatestのみのような運用がなされています. 思想に共感できたり, glibcを使いたい場合などに使う感じです.
  • 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

イメージの仕様を見てみるとコンテナイメージは差分により構成されていることがわかります. 以下のページの図が理解しやすいです.

http://docs.docker.jp/engine/userguide/storagedriver/aufs-driver.html#deleting-files-with-the-aufs-storage-driver

どこの差分がなぜ重くなっているか知ることでより本質的な改善が行えるのではないかと考え, ツールを作りました.

github.com

docker saveで生成されるファイルを分析して, レイヤーごとに差分のサイズとファイル名が出力するツールです.

不明なcacheファイルが残っていることや, 不要なツール, 重複して作られているファイルの存在の認識ができコンテナイメージを小さくする際に力を発揮します.

実際にGoの公式イメージサイズを小さくすることに成功しました. 改善活動をするときは原因特定ができる状況を作りましょう.

ぜひdlayerを使ってコンテナイメージを小さくしてみてください!

余談

このツールが出た少しあとに以下のツールが爆発的に盛り上がりました.

github.com