Dockerイメージの解析—『dive』でレイヤーとファイルを探索、無駄を発見する

Docker Moby logo. Copyright © Docker, Inc.

Dockerを利用する上で、公式の Dockerfileベストプラクティス が触れている通り、Dockerイメージのデータサイズを必要最小限にすることが大切です。

例えば、Dockerイメージを取得してコンテナ起動をするとして、取得するイメージのデータサイズが大きければ大きいほど、コンテナ起動の前段階にあるイメージ取得の時間的オーバーヘッドも大きくなってしまうことが想像できます。

今回紹介する『dive』は、Dockerイメージを構成するレイヤーと、レイヤーに含まれるファイルを閲覧することが可能な解析ツールです。

https://github.com/wagoodman/dive

では、diveを用いてDockerイメージのレイヤーを探索し、不要なファイルなどを特定してみましょう。

diveの実行

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive jenkins/jenkins:lts

まず、diveをコンテナから実行します。

diveをコンテナ実行する上での注意点ですが、diveがローカルのDockerデーモンと通信する関係上、オプション-vでローカルのDockerデーモンのソケットをコンテナ側にマウントしてあげる必要があります。

diveの実行方法は2種類あり、既存のDockerイメージに対しての実行と、DockerfileからビルドされるDockerイメージに対しての実行があります。

今回は前者の方法で、CI/CDサーバJenkinsの公式Dockerイメージのレイヤーを探索してみます。

画面と操作方法

dive, gif animation by asciinema and asciicast2gif
※クリックして拡大

diveテキストベースUIになっており、左セクションにDockerfileコマンドに対応したレイヤーの一覧ビュー、右セクションにレイヤーが含むファイルの一覧ビューが表示されます。

ここからはデフォルトのショートカット設定で説明を進めますが、Tabキーでレイヤーとファイルの一覧ビューの間で操作フォーカスの切り替え、矢印キーとSpaceキーで一覧ビュー内の選択カーソルの移動やディレクトリの伸展・折りたたみができます。

以下が主だったショートカットの抜粋です。

ショートカット 説明
Ctrl + C diveを終了する
Ctrl + L 選択中レイヤーのファイル一覧
Ctrl + A 最初のレイヤーから選択中レイヤーまで集約したファイル一覧
Ctrl + F ファイル名のフィルター
Ctrl + U 変更のないファイルの表示/非表示
Ctrl + B ファイル属性の表示/非表示

https://github.com/wagoodman/dive#keybindings

個人的には、変更のないファイルを非表示にして、Ctrl + Lで選択中のレイヤーと1つ前のレイヤーの間におけるファイル変更差分を探索するのがおすすめです。

Dockerイメージにおける無駄の発見

私見では、『Dockerイメージにおける無駄』の定義は以下になると思っています。

  1. いずれのレイヤーにも存在すべきでないファイル
  2. 最初から最後のレイヤーに至るまで特定ファイルで何度も繰り返される作成・変更・削除アクション
  3. 項番1〜2の結果として存在するレイヤー

項目1は、Ctrl + Lで選択中のレイヤーと1つ前のレイヤーの間におけるファイル変更差分を探索することで発見できます。

ただ、『存在すべきでないファイル』の定義はdiveが探索対象にしているDockerイメージの用途次第なので、どのDockerイメージにも通用する最適な特定方法を挙げるのが難しい。

とはいえ、経験論的な代表アプローチはいくつか存在していて、以下が無駄なファイルと捉えることができます:

  • システムやミドルウェアのログファイルが格納される/var/log/以下のファイル
  • ローケールやmanドキュメントなど『機械』でなく『ヒト』のサポートを目的としたファイル
  • Dockerイメージを最終成果物とした場合、そこに追加のコンテンツを加えうるパッケージマネージャ(e.g. aptdnfnpm)やビルドツール(e.g. gccmakego

項目2〜3は、diveが提供する『イメージの効率推定結果』から判断できます。以下が公式ドキュメントにおける説明の抜粋:

https://github.com/wagoodman/dive#basic-features

Estimate "image efficiency"

[…] and an experimental metric that will guess how much wasted space your image contains. This might be from duplicating files across layers, moving files across layers, or not fully removing files. Both a percentage “score” and total wasted file space is provided.

このイメージの効率推定結果はdiveのテキストベースUI上だと左セクションの下で見切れたり、完全に隠れてしまって見づらいので、実行結果をJSON出力した上でイメージの効率推定結果を確認します。

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $PWD:/data
  wagoodman/dive jenkins/jenkins:lts
  -j /data/result.json

そして出来上がったJSON出力からイメージの効率推定結果を抜粋したものが以下:

"image": {
  "sizeBytes": 450089014,
  "inefficientBytes": 4216729,
  "efficiencyScore": 0.9946289313340139,
  "fileReference": [
    {
      "count": 2,
      "sizeBytes": 1661958,
      "file": "/var/cache/debconf/templates.dat"
    },
    {
      "count": 2,
      "sizeBytes": 1615097,
      "file": "/var/cache/debconf/templates.dat-old"
    },

Dockerイメージ上の特定パスにあるファイルを準備するなら1回のDockerfileコマンドで完全に準備するのが理想と言えますが、プロジェクトやコンテンツのモジュール設計思想や、ビルドツールなどの都合上、アディティブ(加法的)にファイル作成せざるを得ないときは仕方がないかなと思います。

上記は複数あるレイヤーを単一レイヤーに集約することでも解決できて、2022年7月時点で未だ実験的機能である docker build --squashdocker-squash などのツールで実現できます。

ですが、特定目的のファイル群をレイヤーとしてまとめた上でのレイヤー単位の持ち回しの容易さやキャッシュによる再可用性が失われてしまうので、「単一レイヤーに集約して出来上がるDockerイメージが50MBになる」だとかの顕著なサイズ縮小の利点がない限り、余り推奨しません。

余談

以下のツールについても、後日記事を書いてみる予定。