Inuverse Sci. X Tech. Blog

← ブログ一覧

Dockerのimage, container, Dockerfile

#docker

はじめに

Dockerのイメージ(image)、コンテナ(container)、Dockerfileの違いがよく分からなかったので調べてみた。

巷ではよく、コンテナはイメージをレイヤーごとに重ねたもので、Dockerfileは設計書と言われる。 イメージには

  • Linuxファイルシステムや
  • アプリケーションといったファイルやディレクトリ が含まれていて、build/runすることでコンテナという形になり、アプリが実行可能になる。

このような説明を聞いて、私はすっと内容が頭に入ってこなかった。 分からない点は

  • ファイルシステムとは?
  • レイヤーとは何を表しているのか?
  • イメージの集合がコンテナなのか? という点である。

LLMと問答していると、数学の言葉で説明したほうが個人的にわかりやすいと感じたので、この記事にメモした。 先行研究が見つからなかったので、必ずしも正しい話ではないことに注意しながら読んでほしい。

結論をいえば、ファイルシステムとはパスからノードへの部分関数であり、レイヤーとはそのファイルシステム状態への変更操作(差分)である。イメージは差分の列と実行設定の組であり、コンテナはイメージを評価したファイルシステム状態の上に書き込み可能な差分を重ねた実行中の状態である。「イメージの集合がコンテナ」ではなく、「イメージから生成された一時的な実行環境」がコンテナである。

ファイルシステム

パス

パス全体の集合をP\mathcal{P}とすると、その元pPp \in \mathcal{P}は例えば

/bin/sh,/etc/osrelease,tmp/x \mathrm{/bin/sh}, \qquad \mathrm{/etc/os-release}, \qquad \mathrm{tmp/x}

のような名前付きの位置を表す。

対象

ファイルシステム上の対象(ノード)全体の集合をN\mathcal{N}とする。各ノードnNn \in \mathcal{N}には、

n=(type,content,metadata) n = (\mathrm{type}, \mathrm{content}, \mathrm{metadata})

という組みであると考える。ここで、

  • type\mathrm{type}はファイル、ディレクトリ、symlinkなどの種別
  • content\mathrm{content}は内容
  • metadata\mathrm{metadata}は権限、所有者、時刻 を表す。

ファイルシステム

ファイルシステム状態とは、パスからノードへの部分関数

F:PN F : \mathcal{P} \rightharpoonup \mathcal{N}

である。パスがあるからと言って、ファイルが必ずしも存在するとは限らないので、部分関数という取り扱いをしている。すなわち、あるパスppに対して、F(p)F(p)が定義されていないことがある。例えば、次のような例である:

F0={/bin/shnsh,/etc/os-releasenos} F_0 = \{ \text{/bin/sh} \mapsto n_\mathrm{sh}\,, \text{/etc/os-release} \mapsto n_\mathrm{os} \}

これは二つのみ対象が存在するファイルシステム状態である。

/bin/sh
/etc/os-release

という構成のほうが見やすいかもしれない。ここで/bin/shnsh\text{/bin/sh}\mapsto n_\mathrm{sh}は「パス/bin/sh\text{/bin/sh}にノードnshn_\mathrm{sh}が対応している」という意味である。

ファイルシステム状態

ファイルシステム状態全体の集合をF\mathcal{F}と書く。したがって、上記のファイルシステム状態はFFF \in \mathcal{F}であった。

差分

差分δ\deltaとは、ファイルシステム状態を別の状態に移す写像である:

δ:FF \delta: \mathcal{F} \to \mathcal{F}

である。差分全体の集合をΔ\Deltaとし、δ\deltaδΔ\delta \in \Deltaである。差分には「追加」「変更」「削除」の三つがある。

差分:追加

差分「/tmp/x\text{/tmp/x}に内容helloを持つファイルを追加する差分」をδadd\delta_\mathrm{add}とし、状態FFに対して適用したものをδadd(F)\delta_\mathrm{add}(F)としてみよう。これは、状態FFに対して、/tmp/x\text{/tmp/x}が新たに定義された状態を返す。

たとえば、

F0={bin/shnsh} F_0 = \{ \text{bin/sh} \mapsto n_\mathrm{sh} \}

なら、

F1=δ(F0)={bin/shnsh,tmp/xnhello} F_1 = \delta(F_0) = \{ \text{bin/sh} \mapsto n_\mathrm{sh}\,, \text{tmp/x} \mapsto n_\mathrm{hello} \}

である。

差分:削除

差分「/tmp/x\text{/tmp/x}に内容helloを持つファイルを削除する差分」をδdel\delta_\mathrm{del}とする。つまり、

δdel(F1)=F1\{tmp/xnhello}=F0 \delta_\mathrm{del}(F_1) = F_1\backslash \{ \text{tmp/x} \mapsto n_\mathrm{hello} \} =F_0

差分:変更

差分「/tmp/x\text{/tmp/x}に内容helloを持つファイルを変更する差分」をδmod\delta_\mathrm{mod}とする。つまり、

δmod(F1)=F1\{tmp/xnhello}{/tmp/xnhello(new)} \delta_\mathrm{mod}(F_1) = F_1 \backslash \{ \text{tmp/x} \mapsto n_\mathrm{hello} \} \cap \{ \text{/tmp/x} \mapsto n^\mathrm{(new)}_\mathrm{hello} \}

レイヤーとイメージ

Dockerにおけるレイヤーは、この意味での差分δΔ\delta \in \Deltaとして理解できる。すなわち、レイヤーとはファイルシステム状態全体に対する変更操作である。

差分列とイメージ

差分の有限列全体の集合をΔ\Delta^\astとする。その元はL=(δ1,,δn)ΔL = (\delta_1, \ldots , \delta_n) \in \Delta^\astの形をしている。

イメージに埋め込まれる実行設定の集合をE\mathcal{E}としよう。その元eEe \in \mathcal{E}には、たとえば次のようなコマンドが含まれる:

  • CMD
  • ENTRYPOINT
  • ENV
  • WORKDIR
  • USER

イメージ全体の集合を

I=Δ×E \mathcal{I} = \Delta^\ast \times \mathcal{E}

と定める。すなわち、あるイメージIII \in \mathcal{I}I=(L,e)I = (L, e)という組みである。ここで、

  • LLはファイルシステムの差分の列
  • eeは実行設定 を表す。

イメージは「一つの完成済みのディレクトリ」ではなく

  1. ルートファイルシステムを作る差分列
  2. その上で対象がどう起動するかの設定 の組である。

An OCI Image is an ordered collection of root filesystem changes and the corresponding execution parameters for use within a container runtime. This specification outlines the JSON format describing images for use with a container runtime and execution tool and its relationship to filesystem changesets, described in Layers. OCI Image Configurationより引用

Dockerfile

実際にDockerを使うとき、すでに存在するイメージを利用することがある。そのようなイメージをベースイメージIbaseII_\mathrm{base} \in \mathcal{I}とする。たとえば、alpineubuntupython:3.13などは、この意味でのベースイメージである。

DockerfileにあるコマンドFROM alpineは既存のイメージIalpineII_\mathrm{alpine} \in \mathcal{I}を初期として採用する操作である。

COPYADDによって参照される入力ファイル集合をXXとする。これをビルドコンテキストと呼ぶこととする。

Dockerfile DDbuildは、概念的には

BD:I×XI B_D: \mathcal{I} \times X \to \mathcal{I}

である。すなわち、Dockerfileによるビルドはベースイメージとビルドコンテキストを入力として、新しいイメージを返す操作である。したがって、Dockerfileはコンテナの設計書ではなく、厳密にはイメージを生成する規則である。


Dockerfileの各命令をもう少し詳しくみるために、現在のイメージをI=(L,e)I = (L, e)としておこう。

FROM

FROM bは、既存イメージbIb \in \mathcal{I}を初期値として採用する操作である。

COPY

COPY a:tは、ビルドコンテキストのファイルaaをターゲットパスttに配置する差分 δcopy\delta_\mathrm{copy} を生成し、

(L,e)(L+ ⁣ ⁣+[δcopy],e) (L, e) \mapsto (L +\!\!+ [\delta_\mathrm{copy}], e)

へ写す。ここで+ ⁣ ⁣++\!\!+は列の連結を表す。

RUN

RUN cmdは、ビルド時にコマンドを実行し、その結果生じる差分 δrunΔ\delta_\mathrm{run} \in \Delta を追加する操作である。すなわち

(L,e)(L+ ⁣ ⁣+[δrun],e) (L, e) \mapsto (L +\!\!+ [\delta_\mathrm{run}], e)

へ写す。

CMD, ENTRYPOINT, ENV, WORKDIR, USER

CMDENTRYPOINTENVWORKDIRUSERはファイルシステムの差分を増やすのではなく、設定eeを更新する操作である。たとえば、CMD c

(L,e)(L,e) (L, e) \mapsto (L, e^\prime)

という形の更新であり、新しいファイルシステムのレイヤーを作らない。

コンテナ

イメージの評価

イメージI=(L,e)I = (L, e)の差分列L=(δ1,,δn)L = (\delta_1, \ldots, \delta_n)を空のファイルシステム状態FF_\emptysetに順次適用することで得られるファイルシステム状態を

FI=δn((δ1(F))) F_I = \delta_n(\cdots(\delta_1(F_\emptyset))\cdots)

と書く。これはイメージを「評価」した結果であり、読み取り専用として固定される。

書き込み可能レイヤー

コンテナが起動すると、FIF_Iの上に新たな書き込み可能な差分δrwΔ\delta_\mathrm{rw} \in \Deltaが追加される。コンテナ上でのファイル操作はすべてこのδrw\delta_\mathrm{rw}に反映される。コンテナ上でのファイルシステムの実効状態は

δrw(FI) \delta_\mathrm{rw}(F_I)

である。

コンテナ

コンテナ全体の集合をC\mathcal{C}とする。コンテナcCc \in \mathcal{C}

c=(FI, δrw, e, σ) c = (F_I,\ \delta_\mathrm{rw},\ e,\ \sigma)

という組みである。ここで、

  • FIF_Iはイメージから得られる読み取り専用のファイルシステム状態
  • δrw\delta_\mathrm{rw}はコンテナ起動後の書き込み可能な差分(初期値は恒等写像id\mathrm{id}
  • eeは実行設定
  • σ\sigmaはプロセスの実行状態(running\mathrm{running}, stopped\mathrm{stopped}, exited\mathrm{exited}など)

を表す。

docker run

docker runはイメージからコンテナを生成する操作であり、

run:IC \mathrm{run}: \mathcal{I} \to \mathcal{C}

と書ける。具体的には、I=(L,e)I = (L, e)に対して

run(I)=(FI, id, e, running) \mathrm{run}(I) = (F_I,\ \mathrm{id},\ e,\ \mathrm{running})

である。id\mathrm{id}は恒等写像(何も変更しない差分)を表す。

一つのイメージIIから複数のコンテナを起動できる。各コンテナは独立したδrw\delta_\mathrm{rw}を持つため、互いのファイルシステムへの書き込みは干渉しない。

コンテナの停止と削除

docker stopでコンテナを停止しても、δrw\delta_\mathrm{rw}は保持される。docker rmでコンテナを削除すると、δrw\delta_\mathrm{rw}は破棄される。イメージIIは不変であるため、コンテナを削除してもFIF_Iは影響を受けない。

まとめ

本記事で導入した概念を整理する。

概念形式的な定義役割
ファイルシステム状態F:PNF : \mathcal{P} \rightharpoonup \mathcal{N}パスからノードへの部分関数
レイヤー(差分)δ:FF\delta : \mathcal{F} \to \mathcal{F}ファイルシステム状態への変更操作
イメージI=(L,e)Δ×EI = (L, e) \in \Delta^\ast \times \mathcal{E}差分列と実行設定の組
DockerfileBD:I×XIB_D : \mathcal{I} \times X \to \mathcal{I}イメージを生成する規則
コンテナc=(FI, δrw, e, σ)c = (F_I,\ \delta_\mathrm{rw},\ e,\ \sigma)イメージを評価し実行中の状態

これらの関係は次のように図示できる:

X, Ibase BD I run c X,\ I_\mathrm{base} \xrightarrow{\ B_D\ } I \xrightarrow{\ \mathrm{run}\ } c
  • Dockerfileはベースイメージとビルドコンテキストから新しいイメージを生成する規則である。「コンテナの設計書」ではなく、正確には「イメージを生成する規則」である。
  • イメージは差分列と実行設定の組であり、不変(immutable)である。
  • コンテナはイメージを評価したファイルシステム状態FIF_Iの上に書き込み可能な差分δrw\delta_\mathrm{rw}を重ねた、実行中の状態である。コンテナを削除するとδrw\delta_\mathrm{rw}は失われるが、イメージIIは影響を受けない。

← ブログ一覧