Dockerのレイヤーについて調べてみる

## はじめに
Dockerfileをコードレビューしてもらう機会があり、その際に、「`apk add`等のコマンドでパッケージをインストールするときはなるべく１行で書きましょう。」という指摘を頂きました。理由としてはそのほうがレイヤーを少なくすることができ、最終的なイメージを小さく保つことができる。ということですが、なぜレイヤーが増えると、実行しているコマンドが同じでもイメージが大きくなるのかをイメージするために調査してみます。

## そもそもレイヤーって？
dockerイメージは一つのファイルから形成されるのではなく、複数のファイルの層が積み重なってできています。
その１層分のことをレイヤーといいます。
具体的には、dockerFile等で命令を一つ実行する毎にそのアウトプットがレイヤーとして積み重なっていきます。
このような構造を取ることにより、他の人が作成したイメージに独自の命令を追加し、レイヤーを重ねて行くことにより、オリジナルのイメージを作成することができる仕組みになっています。

例えば下記のDockerFileから作成されるDockerImageは下記のイメージになります。
```Dockerfile:Dockerfile
FROM alpine:latest
RUN apk add vim
RUN apk add curl
```
![青 シンプル お知らせ メモ インスタグラム投稿.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499266/a0f2395c-6e60-3767-ee49-c5e9c26dd38b.png)

## 実際に調査してみる。
#### まずは実行
調査対象としたDockerfileは下記の２つです。
単純にalpineのコンテナに、vimとcurlをインストールしてみます。
これらのインストールを１行で行うパターンと２行で行うパターンを比較してみます。
- インストールを1行でするversion(以下、one_liner)
```Dockerfile:Dockerfile
FROM alpine:latest
RUN apk update
RUN apk add vim && apk add curl
```

- インストールを2行でするversion(以下、two_liners)
```Dockerfile:Dockerfile
FROM alpine:latest
RUN apk update
RUN apk add vim
RUN apk add curl
```

これをそれぞれビルドします。
```zsh:shell
docker build . -t test:one_liner
```
```zsh:shell
docker build . -t test:two_liners
```
できたイメージがこちら
```zsh:shell
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のほうが少し大きい...?

#### イメージの中身を見てみる
イメージを取り出すコマンドは公式のリファレンスにあったのでそちらを使用します。

https://docs.docker.jp/engine/reference/commandline/image_save.html
```zsh:shell
# 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にレイヤー関連の情報が記載されているみたいでした。
```zsh:shell
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の要素数も３つになっています。そのため３層になっているのがわかります。

一応実際のファイルサイズがmanifest.jsonと一致しているかも確認。
```zsh:shell
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を見てみます。
```zsh:shell
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の要素数を見ると４つになっているので４層になっていることがわかります。
#### 中身を見てみる
いろいろ調べていると、manifest.jsonのLayersの順番にDockerfileで記載したコマンドによって作成されたレイヤーが対応しているみたいでした。
そのため、one_linerのLayersの3行目のファイルを展開すれば、vimとcurlのファイルが確認できるはず。
```zsh:shell:/one_liner
# 展開用にディレクトリを作成
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側の性能改善によって昔ほどレイヤー数を無理に減らさなくても良いということなのかもしれないのかなと解釈しました。
そのため、単純にモジュールを追加していくだけの処理であれば、そこまで容量に差がでないのかもしれませんね。
とはいえ、途中でファイルの削除等を挟む場合には、今でも１行で書いたほうがサイズを減らせるのではないかとも思いました。


## まとめ
今回の調査によって、１行で書くことにより劇的にサイズを少なくするという期待する結果を得ることはできませんでしたが、dockerのレイヤーがどのようなものなのかを具体的に見ることができ、dockerに対する理解が深まった気がしました。
また、今回はパッケージを２つだけインストールした場合で検証しましたが、更に多くのパッケージをインストールする場合等に効果を発揮するのかもしれません。
余裕があったらまた検証にチャレンジしてみたいと思います！