🐳

【あえての深堀調査】Dockerイメージとは何か?調べてイメージを作ってみる

に公開

【あえての深堀調査】Dockerイメージとは何か?調べてイメージを作ってみる

はじめに

三菱UFJインフォメーションテクノロジーの谷川と申します。
社内アプリケーションのインフラ保守・開発やCI/CDフローの改善などに従事しています。
また最近はAIを活用したツールの開発にも一部携わっています。

本記事は社内のアドベントカレンダーで投稿した内容です。弊社内で実施されたアドベントカレンダーの記事がどんなものだったかをご紹介できたらと思います!
(社内でのアドベントカレンダー企画については、下記の記事をご覧ください)
過去記事 : アドベントカレンダー企画を会社内でやってみての振り返り

概要

あなたはDockerイメージを作ることができますか?

Dockerfileを書いて、docker buildでビルドできるので、普段コンテナを扱っている人なら難しくはないと思います。
ですが、それは恐らくFROMにベースイメージを指定して、そこに何かを足すことがほとんどではないでしょうか?

本当の意味で「Dockerイメージを作れるよ!」と言いたかったので、Dockerイメージの実態を調べてゼロからDockerイメージを作ってみよう、という試みです。

この記事の概要

この記事ではDockerの内部構造を解き明かすために、dockerコマンドを使わずにイメージを作ります。
セキュリティを高めるためにログインシェルが不要ならDistrolessを使うことになりますし、余計なものが何もいらなければscratch(空のイメージ)を利用します。

Dockerイメージとは

Dockerイメージを作るにあたり、Dockerイメージを構成する要素について説明します。

Dockerfile

Dockerfileとは、Dockerイメージのビルドに利用する命令セットです。
例えば、Javaのアプリケーションを稼働させるイメージを、RedHatのUBIをベースイメージから作る場合は、以下のようなDockerfileを記載します。

FROM redhat/ubi8:8.9
RUN dnf install -y java-17-openjdk
WORKDIR /app
COPY penguinApp.jar /app/penguinApp.jar
ENTRYPOINT ["java", "-jar", "/app/penguin.jar"]

Dockerfileを書くためのベストプラクティス

公式ドキュメントに詳細があります。
イメージを扱う上で大切なことが色々書いているので、一度は目を通しておきたいですが、要点としては以下です。

  1. 1つのコンテナに対して、目的(プロセス)は1つとする
  2. 無駄なものを作らず、限りあるリソースを有効活用する

簡単に見えますが、これが意外と難しく...本記事では後者に焦点を当てます。

レイヤ

「無駄なものを作らない」で、まず挙げられるのは「レイヤ」です。
Dockerイメージは「レイヤ」の集合として構成されています。

Dockerfileの命令1行が、大体1レイヤに相当します。
例えば、先ほどのDockerfileを元に作ったイメージは、以下のようなレイヤ構成となります。

dockerimage.png

作成するDockerイメージのレイヤは極力減らす

ガイド中のベストプラクティスでも触れられていますが、レイヤの数は極力減らすべきです。
レイヤ数を少なくすることには、以下のメリットがあります。

  • イメージサイズの縮小
  • コンテナ起動やビルド処理時間の短縮
  • キャッシュの効率化

ちなみに、レイヤを増やす命令はCOPY,ADD,RUNの3つです。
レイヤを増やさないためには、以下を意識すると良いでしょう。

  • 可能であれば、1行にまとめる
    • RUNが連続する場合、&でまとめる(RUN mkdir /app & cd /app )
  • レイヤを増やさない命令を活用する
    • 先ほどのDockerfile内のWORKDIRRUN mkdir /app & cd /app に相当します。
      WORKDIRに置き換えることで、前のレイヤのメタデータとして持てるので、レイヤを減らせます。

(メンテナンス性を高めるために、意図的にレイヤを分離することもあります)

Dockerイメージの構成を見る

イメージはtarファイルに出力できるため、それを解凍することで、内部構造を確認していきます。
ここではRedHatのUBIイメージを利用していきます。

docker save redhat/ubi8:8.9 > ./ubi.tar
tar xf ./ubi.tar

解凍結果

blobs/
└── sha256/
    ├── 053de0cbf9f05272ca7279809c748f87bf619ab9a2c0d6dc4fc902df60959f16
    ├── 388e2b6b84a27760caca69f14e4a104a742c222bde88e87b2cc7d92ff378858f
    ├── 7cb2198f105bc6e59faaa598b914fb470c8dc156e75a85234addf42d0c4fd740
    └── c70762f1df383623777503c532e9cd575d9c75d41edbfaa4739e6a10aeab30a7
index.json
manifest.json
oci-layout
repositories

※もしもレイヤが多い場合は、その分レイヤとメタデータが増えるため、sha256直下のファイル数が増えます。

manifest.jsonを確認すると、7cb2198f105bc6e59faaa598b914fb470c8dc156e75a85234addf42d0c4fd740がメインのレイヤのようです。加えてこちらもtarファイルのようなので、解凍していきます。

//manifest.json
[
  {
    "Config": "blobs/sha256/388e2b6b84a27760caca69f14e4a104a742c222bde88e87b2cc7d92ff378858f",
    "RepoTags": [
      "redhat/ubi8:8.9"
    ],
    "Layers": [
      "blobs/sha256/7cb2198f105bc6e59faaa598b914fb470c8dc156e75a85234addf42d0c4fd740"
    ],
    "LayerSources": {
      "sha256:7cb2198f105bc6e59faaa598b914fb470c8dc156e75a85234addf42d0c4fd740": {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",
        "size": 212797440,
        "digest": "sha256:7cb2198f105bc6e59faaa598b914fb470c8dc156e75a85234addf42d0c4fd740"
      }
    }
  }
]

ここではレイヤやメタデータ等の情報の在処を扱っているようです。

解凍すると、Dockerコンテナ内のファイル構造が見られました。

lrwxrwxrwx  1 root root         7  6月 21  2021 bin -> usr/bin
dr-xr-xr-x  2 root root         6  6月 21  2021 boot
drwxr-xr-x  2 root root         6 11月  1  2023 dev
drwxr-xr-x 47 root root      4096 11月  1  2023 etc
drwxr-xr-x  2 root root         6  6月 21  2021 home
lrwxrwxrwx  1 root root         7  6月 21  2021 lib -> usr/lib
lrwxrwxrwx  1 root root         9  6月 21  2021 lib64 -> usr/lib64
drwx------  2 root root         6 11月  1  2023 lost+found
drwxr-xr-x  2 root root         6  6月 21  2021 media
drwxr-xr-x  2 root root         6  6月 21  2021 mnt
drwxr-xr-x  2 root root         6  6月 21  2021 opt
drwxr-xr-x  2 root root         6 11月  1  2023 proc
dr-xr-x---  3 root root       213 11月  1  2023 root
drwxr-xr-x  4 root root        33 11月  1  2023 run
lrwxrwxrwx  1 root root         8  6月 21  2021 sbin -> usr/sbin
drwxr-xr-x  2 root root         6  6月 21  2021 srv
drwxr-xr-x  2 root root         6 11月  1  2023 sys
drwxrwxrwt  2 root root        58 11月  1  2023 tmp
drwxr-xr-x 12 root root       144 11月  1  2023 usr
drwxr-xr-x 19 root root       249 11月  1  2023 var

サイズが0のディレクトリを除くと、以下のディレクトリに絞られました。UBIのイメージはレイヤ1つで構成されるため、イメージの実態はこれらのディレクトリ群であることが分かりました。

$ du -h --max-depth=1 ./ | grep -v ^0
3.1M    ./etc
60K     ./root
8.0K    ./tmp
200M    ./usr
13M     ./var
216M    ./

なんだかDockerイメージって、ブラックボックスな印象でしたが、中にはJSONとファイルシステムしか存在しないようです。(もっとバイナリとかあるのかと想像していました)

実際につくってみた

作るために必要なもの

docker saveで出力したtarはdocker loadで読み込めます。
つまり、これまでの情報があれば、ゼロから作れるだろう...ということで、作ってみます。
試行錯誤した結果、コンテナを作るために、最低限必要なものは以下だとわかりました。

blobs/
└── sha256/
    ├── レイヤのデータ(tar)
    └── レイヤのメタデータ(json)
manifest.json # イメージのメタデータ。構成情報が書かれている
repositories # リポジトリ名とタグ名の情報を内包。(manifest.jsonに"RepoTags"を設定したから必要だったのかも)

手順

稼働確認のためにHelloWorldを表示するシングルバイナリを配置し、これをメインプロセスとする「penguin/original:1.0」というコンテナイメージを作ってみます。

  1. ディレクトリを作成する

    $ mkdir -m 755 -p ./original_image/blobs/sha256
    $ cd ./original_image
    
  2. バイナリファイルをアーカイブする(【sha256sumの値①】を後手順で利用します)

    ※バイナリがカレントディレクトリにあると仮定
    $ tar cf .binary.tar ./hello
    $ sha256sum ./binary.tar
    $ mv ./binary.tar ./blobs/sha256/【sha256sumの値①】
    
  3. configを作成する(【sha256sumの値②】を後手順で利用します)

    $ vi ./json
    

    ./jsonに、以下の内容(JSON)を記載する

    {
      "architecture": "amd64",
      "created": "0001-01-01T00:00:00Z",
      "os": "linux",
        "diff_ids": [
          "sha256:blobs/sha256/【sha256sumの値①】"
        ]
      },
      "config": {
        "User": "0",
        "WorkingDir": "/"
      }
      "rootfs": {
        "type": "layers",
        "diff_ids": [
          "sha256:【sha256sumの値①】"
        ]
      }
    }
    
    $ sha256sum ./json
    $mv ./json ./blobs/sha256/【sha256sumの値②】
    
  4. manifest.jsonを作る

    $ vi manifest.json
    

    中身は以下の通り

    [
      {
        "Config": "blobs/sha256/【sha256sumの値②】",
        "RepoTags": [
          "penguin/original:1.0"
        ],
        "Layers": [
          "blobs/sha256/【sha256sumの値①】"
        ]
      }
    ]
    
  5. イメージ名やタグを設定するためのrepositriesを作る

    $ vi repositories
    

    内容は以下。penguin/originalがイメージ名で1.0がタグ

    {
      "penguin/original": {
        "1.0": "【sha256sumの値①】"
      }
    }
    
  6. すべてをアーカイブし、Dockerイメージとする。そしてロードする

    $ tar cf /tmp/original_image.tar ./*
    $ docker load < /tmp/original_image.tar
    

以上で完了です。

実際にdocker imagesで確認してみると、無事に読み込まれています!(metadataがないので作成日はN/Aになる)

$ docker images penguin/original:1.0
REPOSITORY	      TAG	    IMAGE ID     CREATED	SIZE
penguin/original	1.0	662936f8add1	N/A     2.02MB

コンテナも正常に動きました。

$ docker run --rm penguin/original:1.0 /hello
++(・0・)++ < Hello Penguin

さいごに

Dockerを初めて触るようになって5年以上経ちましたが、イメージの内部構造まで詳しく見る機会はなく...今回の検証を通して、よりDockerへの理解が深まりました。
今後はレイヤの構成も意識して、軽量かつメンテナンス性の高いイメージ運用に役立てたいです。

Discussion