知らないと損する Docker イメージのレイヤ構造とは

こんにちは、Synergy! 開発チームの松本です。これは TECHSCORE Advent Calendar 2018 の 10 日目の記事です。

Docker イメージをビルドした結果、作成されたイメージのサイズが大きすぎて困った経験はないでしょうか。コンパクトなイメージを作成するには、イメージの構造を知ることが鍵となります。本稿は、その前提知識となるイメージ内部についての解説記事です。

なお、本記事内で利用している Docker のバージョンは次の通りです。

ユニオンファイルシステムとコピーオンライト

Docker コンテナが軽量だと言われる理由のひとつが、ユニオンファイルシステムにあります。ユニオンファイルシステムは、複数のファイルシステム上のディレクトリやファイルをレイヤとして重ね合わせ、それらを仮想的に一つのファイルシステムとして扱う技術です。

Docker は、このユニオンファイルシステムを使い、イメージを複数の読み取り専用レイヤのスタックとして扱います。コンテナ内ではさらに、読み書き可能なレイヤを一枚重ねています。

コンテナ上の読み書き可能なレイヤをコンテナレイヤと呼び、イメージを構成する読み込み専用レイヤをイメージレイヤと呼びます。

この構成により、同一ノード上で動く複数のコンテナが、Docker イメージを構成するイメージレイヤを共有することを可能にし、トータルとしてのコンテナサイズを最小化しているのです。

共有しているイメージレイヤに対するコンテナ上でのファイルの更新は、コピーオンライトによって実現しています。

コピーオンライトでは、イメージレイヤに存在するファイルへの更新時に、イメージレイヤからデータをコンテナレイヤーにコピーして変更を反映します。対象となるデータが複数のイメージレイヤに存在する場合は、最も上位のイメージレイヤ内のデータがコピーされます。

コピーオンライト処理は、利用している環境が選択しているストレージドライバによって実装が異なります。例えば aufs ドライバではコピーされるデータはファイルそのものですが、devicemapper ドライバではコピーされるデータは 64K バイトのブロック単位となります。

サポートされるストレージドライバのリストは次の通りで、利用している環境や用途に応じてベストなドライバを選択できます。

  • overlay2, overlay
  • aufs
  • btrfs
  • devicemapper
  • vfs
  • zfs

選択中のドライバが何であるかは、docker info コマンドで確認できます。以下の例では devicemapper ドライバが選択されているとわかります。

イメージのレイヤ構成

Docker イメージ内のレイヤ構造は docker history コマンドで確認できます。下の例では、ubuntu:18.04 が 5 つのレイヤで構成されていることをうかがえます。

このイメージをビルドする時に利用された Dockerfile と見比べてみましょう。各行は、Dockerfile 内の RUNADD などの命令(instruction)に対応しており、一番下の行が、Dockerfile 内での ADD ubuntu-bionic-core-cloudimg-amd64-root.tar.gz / を実行した結果として作成されたレイヤです。最上位のレイヤはイメージ ID と一致しています。

この ubuntu:18.04 をもとに新たなイメージを作成するとレイヤ構成がどうなるか試してみます。Dockerfile は次の通り。

ビルドします。

レイヤ構成を見てみます。

先ほどの ubuntu:18.04 の上に、12 バイトの新たなレイヤ 16f4672a0118 が重なり、それが Dockerfile に記述した RUN 命令の結果として作成されたものであることがわかります。

このように、Docker イメージのビルドは、ベースとして利用したイメージの上に新たなレイヤを追加するだけとなります。

イメージレイヤのごみファイル問題

前述の通り Docker イメージは、Dockerfile に記述された RUNADD 命令などを実行した結果、ファイルシステム上に加えられた変更をレイヤとして生み出します。このため、必要となる最終成果物を生成する途中で生み出された不要なファイルがレイヤに含まれて、レイヤのサイズが大きくなってしまうことがあります。

実験してみましょう。

Ubuntu で、apt-get install する前に apt-get update することはよくあります。下の例では apt-get update でパッケージリストの更新を行うだけの Dockerfile を作成し、ビルドします。

Dockerfile の内容はこれです。

ビルドします。

レイヤの中身を確認するために、イメージを docker save コマンドでアーカイブします。

manifest.jsonrepositories のタイムスタンプがおかしいですが、本筋に関係ないので無視しましょう。

layer.tar の中に、レイヤ内のファイルが格納されています。ベースとなった ubuntu:18.04 のイメージに含まれていたレイヤを除き、新たに追加されたのは、973e978733afe5edf37c0222 からはじまるディレクトリ内の layer.jar です。この中で、RUN apt-get update で作成されたレイヤは、e5edf37c0222 ではじまるディレクトリの layer.tar です。中身を確認してみます。

当たり前の話ですが、このように apt-get upadte コマンドによって取得したパッケージリストがレイヤの中に含まれています。しかし多くの場合、これらのファイルはイメージサイズを大きくしているだけの存在で、コンテナの実行には不要です。

では次は、この不要なパッケージリスト(やキャッシュ)を削除する RUN を追加した Dockerfile を用意し、ビルドして中身を見てみます。

ビルドします。

余談ですが Step 2/3 を見ると、先ほど作成した apt-get-update-1:latest が利用されていることが確認できます。

アーカイブして中身を確認します。

ベースとなった ubuntu:18.04 のイメージに含まれていたレイヤを除き、新たに追加されたのは、973e978733afcb7480f242bcf2bc08b32ca8 からはじまるディレクトリです。

f2bc08b32ca8layer.jar を見ると、先ほどと同じように apt-get upadte コマンドによって取得したパッケージリストが含まれてしまっています。

「削除されてないじゃないか」と思うかもしれませんが、cb7480f242bclayer.jar を見てください。

.wh. をプレフィクスとしたファイルリストがサイズ 0 で含まれています。これがホワイトアウトファイルと呼ばれる、削除したファイルのリストです。このレイヤを重ねることで、下層レイヤのファイルを削除したことにしているのです。

しかし、これでは余分にレイヤが増えただけで、全体としてのイメージサイズが小さくなっていません。

次は RUN 命令一行で Dockerfile を書いてみます。

ビルドします。

アーカイブして、内容を確認します。

新たに追加されたのは、df0a6127ae8b から始まるディレクトリのみですが、この中の layer.jar にはパッケージリストは入っていません。

今回は、期待通りごみファイルがレイヤごと消えてなくなりました。Docker イメージのビルドでは、ごみファイルが残らないよう、常に注意が必要ですね。

イメージレイヤのゴミファイル問題 その 2 (マルチステージビルド)

先ほどの apt-get update のようなケースでは、&& でコマンドをつなぐだけで対応できますが、&& で対応するには複雑になりすぎるようなケースもあり得ます。以前はビルダーパターンを使った、少し面倒な方法で回避していましたが、Docker 17.05 以降では、マルチステージビルドで対応できまるようになりました。

マルチステージビルドでは、ひとつの Dockerfile 内で、複数の FROM 命令が使えます。これがマルチステージと呼ばれる理由です。

各ステージでは、前のステージでビルドされた成果物を参照でき、最終ステージのみがイメージとして保存されます。

次の例は、docker ユーザーガイドからの引用です。

ひとつめのステージは、FROM 命令に builder という名前が付けられています。それをふたつめのステージの COPY 命令の --from=builder で参照していることがわかります。これにより、ひとつめのステージの成果物である app を、ふたつめのステージにコピーすることを可能にし、必要なファイルのみをイメージ内に含めることができるというわけです。

最後に

Docker に関するブログネタについては、他にも書きたいことがいくつかありました。コンテナ内でホスト OS とは異なる OS をどのように動かしているのかも、そのひとつです。機会があれば書いてみようと思っていますが、そう思って書いたことがないのが現実です。われながら移り気なものです……。

Comments are closed, but you can leave a trackback: Trackback URL.