はじめに
Dockerfileをコードレビューしてもらう機会があり、その際に、「apk add
等のコマンドでパッケージをインストールするときはなるべく1行で書きましょう。」という指摘を頂きました。理由としてはそのほうがレイヤーを少なくすることができ、最終的なイメージを小さく保つことができる。ということですが、なぜレイヤーが増えると、実行しているコマンドが同じでもイメージが大きくなるのかをイメージするために調査してみます。
そもそもレイヤーって?
dockerイメージは一つのファイルから形成されるのではなく、複数のファイルの層が積み重なってできています。
その1層分のことをレイヤーといいます。
具体的には、dockerFile等で命令を一つ実行する毎にそのアウトプットがレイヤーとして積み重なっていきます。
このような構造を取ることにより、他の人が作成したイメージに独自の命令を追加し、レイヤーを重ねて行くことにより、オリジナルのイメージを作成することができる仕組みになっています。
例えば下記のDockerFileから作成されるDockerImageは下記のイメージになります。
FROM alpine:latest
RUN apk add vim
RUN apk add curl
実際に調査してみる。
まずは実行
調査対象としたDockerfileは下記の2つです。
単純にalpineのコンテナに、vimとcurlをインストールしてみます。
これらのインストールを1行で行うパターンと2行で行うパターンを比較してみます。
- インストールを1行でするversion(以下、one_liner)
FROM alpine:latest
RUN apk update
RUN apk add vim && apk add curl
- インストールを2行でするversion(以下、two_liners)
FROM alpine:latest
RUN apk update
RUN apk add vim
RUN apk add curl
これをそれぞれビルドします。
docker build . -t test:one_liner
docker build . -t test:two_liners
できたイメージがこちら
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test two_liners xxxxxxxxxxxx 2 hours ago 45.6MB
test one_liner xxxxxxxxxxxx 2 hours ago 45.5MB
確かにtwo_linersのほうが少し大きい...?
イメージの中身を見てみる
イメージを取り出すコマンドは公式のリファレンスにあったのでそちらを使用します。
# image展開用のディレクトリを作成
mkdir one_liner
# imageをセーブ(imageはtarでアーカイブされたものが出力される)
docker image save test:one_liner -o one_liner/image.tar
# ディレクトリを移動
cd one_liner
# tarコマンドで展開
tar -zxf image.tar
# 展開後の一覧を表示
ls ─╯
blobs image.tar index.json manifest.json oci-layout repositories
展開後のファイルを色々見ていると、どうやら、blobs/sha256
配下に実際のレイヤーが格納されているのと、manifest.jsonにレイヤー関連の情報が記載されているみたいでした。
cat manifest.json | jq ─╯
[
{
"Config": "blobs/sha256/6c8dbfcfbca671b0b3b9334a41ecebd5201c9666d90af316ccf40532d8b22aba",
"RepoTags": [
"test:one_liner"
],
"Layers": [
"blobs/sha256/977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d",
"blobs/sha256/3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630",
"blobs/sha256/be4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b"
],
"LayerSources": {
"sha256:3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 2472960,
"digest": "sha256:3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630"
},
"sha256:977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 8464384,
"digest": "sha256:977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d"
},
"sha256:be4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 36279808,
"digest": "sha256:be4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b"
}
}
}
]
確かにone_linerはDockerfileを3行で書いているのでLayersの要素数も3つになっています。そのため3層になっているのがわかります。
一応実際のファイルサイズがmanifest.jsonと一致しているかも確認。
ls -l blobs/sha256 ─╯
total 92272
-rw-r--r--@ 1 mame staff 870 12 8 21:52 077e400a38f167478bf9b43083641094238e96118e7e9abd3cf148300e3bf892
-rw-r--r--@ 1 mame staff 401 12 8 21:52 1b383c5862d3a41436494b30f284c480904acd3476718431f738360bc35d50a8
-rw-r--r--@ 1 mame staff 2472960 12 8 21:52 3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630
-rw-r--r--@ 1 mame staff 708 1 1 1970 6c6130efc494de845d6254dc7f4df3ff092b1d391766f88e69ffd1ab17026690
-rw-r--r--@ 1 mame staff 1046 12 8 21:52 6c8dbfcfbca671b0b3b9334a41ecebd5201c9666d90af316ccf40532d8b22aba
-rw-r--r--@ 1 mame staff 477 12 8 21:52 8695a2373fe88ff4cb060da25e3dce2bdc83fde38c5978a1d000c5bb603d8c7f
-rw-r--r--@ 1 mame staff 8464384 12 8 21:52 977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d
-rw-r--r--@ 1 mame staff 36279808 12 8 21:52 be4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b
こちらもmanifest.jsonののsizeと値が一致しているのを確認できました。manifest.jsonを見るといろいろわかりそうですね。
同様にtwo_linersのmanifest.jsonを見てみます。
cat manifest.json | jq
[
{
"Config": "blobs/sha256/6542b0cf7df81f5be54a454f9868324d6e60275fceaf9b9b8aa1027ffda895cb",
"RepoTags": [
"test:two_liners"
],
"Layers": [
"blobs/sha256/977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d",
"blobs/sha256/3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630",
"blobs/sha256/744626c64fed99ee7dcf6e2cda66e1cc8c635d5b7b0aaeda579a86afea1fd603",
"blobs/sha256/6f52a0874715aaaddd490e8b774db30145ff83043db79319fc37d9cc09e74754"
],
"LayerSources": {
"sha256:3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 2472960,
"digest": "sha256:3c7a6a496478c1d53ad62f997dda2bc0e55440f35c1bca960ad1af48c4edd630"
},
"sha256:6f52a0874715aaaddd490e8b774db30145ff83043db79319fc37d9cc09e74754": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 5219840,
"digest": "sha256:6f52a0874715aaaddd490e8b774db30145ff83043db79319fc37d9cc09e74754"
},
"sha256:744626c64fed99ee7dcf6e2cda66e1cc8c635d5b7b0aaeda579a86afea1fd603": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 31184384,
"digest": "sha256:744626c64fed99ee7dcf6e2cda66e1cc8c635d5b7b0aaeda579a86afea1fd603"
},
"sha256:977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 8464384,
"digest": "sha256:977340364f395522c48194db0cf2a81b7643d1bd1378a4c16dc848095a39de7d"
}
}
}
]
こちらも、Layersの要素数を見ると4つになっているので4層になっていることがわかります。
中身を見てみる
いろいろ調べていると、manifest.jsonのLayersの順番にDockerfileで記載したコマンドによって作成されたレイヤーが対応しているみたいでした。
そのため、one_linerのLayersの3行目のファイルを展開すれば、vimとcurlのファイルが確認できるはず。
# 展開用にディレクトリを作成
mkdir vim_curl
# 上記で作ったディレクトリに展開
tar -zxf blobs/sha256/be4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b -C vim_curl
# vim_curlディレクトリの中身を確認
ls vim_curl
etc lib usr
# 更に中身を確認
ls vim_curl/usr/bin/
curl ex rview rvim view vim xxd
確かに、vimとcurlがインストールされていることがわかりました。そのため調査対象は間違っていなさそうです。
サイズを比較してみる
ここまでの調査からone_liner3行目のbe4fa7e81edbbbf5ed6ccc2648e77f4cf69e3dcf82a9dc5c7514d59676437f4b
とtwo_linersの3行目と4行目である744626c64fed99ee7dcf6e2cda66e1cc8c635d5b7b0aaeda579a86afea1fd603
,6f52a0874715aaaddd490e8b774db30145ff83043db79319fc37d9cc09e74754
の合計値との比較がサイズの比較になりそうです。
- 計算
(31184384 + 5219840) - 36279808 = 124416(byte)
124416/1024 = 121.5(kb)
なので121.5kb差となりました。MB単位で表示したときに0.1MB差だったこととも結果はあっていそうです。
考察
思った程差がでないなと思ってちょっとがっかりしていましたが、公式ドキュメントのベストプラクティスのページを読んでいるとこんな記載がありました。
Docker の古いバージョンでは、性能を確保するために、イメージ・レイヤ数の最小化が重要でした。以下の機能は、この制限を減らすために追加されたものです。
RUN 、 COPY 、 ADD 命令のみレイヤを作成します。他の命令では、一時的な中間イメージ(temporary intermediate images)を作成し、構築時の容量は増えません。
上記の記載から、docker側の性能改善によって昔ほどレイヤー数を無理に減らさなくても良いということなのかもしれないのかなと解釈しました。
そのため、単純にモジュールを追加していくだけの処理であれば、そこまで容量に差がでないのかもしれませんね。
とはいえ、途中でファイルの削除等を挟む場合には、今でも1行で書いたほうがサイズを減らせるのではないかとも思いました。
まとめ
今回の調査によって、1行で書くことにより劇的にサイズを少なくするという期待する結果を得ることはできませんでしたが、dockerのレイヤーがどのようなものなのかを具体的に見ることができ、dockerに対する理解が深まった気がしました。
また、今回はパッケージを2つだけインストールした場合で検証しましたが、更に多くのパッケージをインストールする場合等に効果を発揮するのかもしれません。
余裕があったらまた検証にチャレンジしてみたいと思います!