概要
DockerではAUFSという技術が使われています。
こちらはUnionFS(ディレクトリを重ね合わせることができる)の一つで、親のファイルシステムをすべてReadOnlyにして、その上に書き込み可能なレイヤを重ねて1つのファイルシステムのように扱います。

Dockerfileで作成する際も、各行がレイヤとして1つ前のコンテナとの差分を保持して作成されます。なので途中で失敗しても、実行中のレイヤのみ破棄するので再実行が非常に高速です。
今回はそのUnionFSについてまとめてみました。
環境
UnionFSとは
複数のファイルシステムを一つの場所にマウントすることができます。例としては以下のように複数のフォルダがある状態で
├── folder001
│ └── foo
└── folder002
├── bar
└── baz
UnionFSを使うことで以下のように1つの場所にまとめてマウントできます。
folder100/ ├── bar ├── baz └── foo
インストール
$ sudo apt-get install unionfs-fuse
使い方
$ unionfs-fuse フォルダ=パーミッション:フォルダ=パーミッション 統合用フォルダ
として使います。先のフォルダほど優先されます。先ほどの例だと以下になります。
$ unionfs-fuse folder001=RW:folder002=RW folder100
こうすると以下のようにまとまります。
.
├── folder001
│ └── foo
├── folder002
│ ├── bar
│ └── baz
└── folder100
├── bar
├── baz
└── foo
パーミッションについてはRWはRead, Write可能。ROはReadOnlyで読み込みのみです。
ケーススタディ
folder001に書き込んだらどうなるか
folder100にも表示されます。
$ touch folder001/hoge
$ tree
.
├── folder001
│ ├── foo
│ └── hoge
├── folder002
│ ├── bar
│ └── baz
└── folder100
├── bar
├── baz
├── foo
└── hoge
folder100に追加したらどうなるか
先のフォルダが優先されるので、folder001の方に追加されます。
$ touch folder100/fuga
$ tree
.
├── folder001
│ ├── foo
│ └── fuga
├── folder002
│ ├── bar
│ └── baz
└── folder100
├── bar
├── baz
├── foo
└── fuga
各フォルダに同じ名前のファイルがある
以下の様な場合、どうなるでしょうか
├── folder001 │ └── foo // 001と書かれている └── folder002 ├── bar └── foo // 002と書かれている
こちらも先ほど述べたように、先のフォルダが優先されるので、先のフォルダの方が表示されます。
$ unionfs-fuse folder001=RW:folder002=RW folder100 $ cat folder100/foo 001
この状態でファイルを変更しても、同じく先のファイルのみ変更されます。
$ echo 100 > folder100/foo $ cat folder001/foo 100 $ cat folder002/foo 002
Copy On Write
次は Copy on Write を検証します。この考え方がDockerのレイヤ構造と同じです。
newという新しいレイヤを追加したとします。そして過去のレイヤをROで保持します。
$ unionfs-fuse -o cow new=RW:folder001=RO:folder002=RO folder100
ここで注意なのが2点あり、
cowオプションを付けるRWのレイヤを一番先頭に持ってきてくる
です。特に後者は見落としがちなので注意です。ROが先にあると、書き込めないのにそちらが優先されるためPermission Deniedになります。
この状態でfolder100にファイルを追加したり、更新したりすると new にのみその更新が入ります。
ファイル追加の場合
$ touch folder100/hoge
$ tree
.
├── folder001
│ └── foo
├── folder002
│ ├── bar
│ └── baz
├── folder100
│ ├── bar
│ ├── baz
│ ├── foo
│ └── hoge
└── new
└── hoge
ファイル更新の場合
別フォルダにあったファイルをfolder100から更新した場合、その変更がnewへ反映されます。
$ echo change! > folder100/foo
$ tree
.
├── folder001
│ └── foo
├── folder002
│ ├── bar
│ └── baz
├── folder100
│ ├── bar
│ ├── baz
│ └── foo
└── new
└── foo
ファイルの更新もfoler001はされず、newにのみ入ります。
$ cat folder001/foo 001 $ cat new/foo change!
まとめ
このようにして親コンテナをReadOnlyにし、書き込み可能なレイヤを作成し、差分のみ保持していきます。 これはDockerに限らず、
- マウント元を変更させたくない
- 読み取り専用メディアにあるので変更できない
- 差分バックアップしたい
といったケースでも利用可能な技術です。