Linux で利用できるファイルシステムの一つに Overlay Filesystem (OverlayFS) がある。 このファイルシステムは、Docker が推奨しているストレージドライバの overlay2 が利用していることで有名。 今回は、そんな OverlayFS を Docker を介さずに扱ってみる。
使った環境は次のとおり。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS" $ uname -rm 5.11.0-1021-gcp x86_64 $ sudo docker version Client: Docker Engine - Community Version: 20.10.9 API version: 1.41 Go version: go1.16.8 Git commit: c2ea9bc Built: Mon Oct 4 16:08:29 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.9 API version: 1.41 (minimum version 1.12) Go version: go1.16.8 Git commit: 79ea9d3 Built: Mon Oct 4 16:06:37 2021 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.4.11 GitCommit: 5b46e404f6b9f661a205e28d59c982d3634148f8 runc: Version: 1.0.2 GitCommit: v1.0.2-0-g52b36a2 docker-init: Version: 0.19.0 GitCommit: de40ad0
もくじ
下準備
OverlayFS は Linux カーネルに組み込まれているため、利用する上で特に必要なパッケージはない。 しいていえば、mount(8) がないとユーザ空間から簡単に操作する手段がないくらい。 ただし、今回は最終的に Docker イメージを元に手動で OverlayFS をマウントして unshare(1) でコンテナもどきを作りたい。 そのために、あらかじめ jq と Docker をインストールしておく。
まずは jq を入れる。
$ sudo apt-get update $ sudo apt-get install jq
続いて Docker を入れる。
$ sudo apt-get remove docker docker-engine docker.io containerd runc $ sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg $ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null $ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io
Docker が動作することを確認しておく。
$ sudo docker run hello-world ... (snip) ... Hello from Docker! This message shows that your installation appears to be working correctly. ... (snip) ...
これで準備が整った。
OverlayFS を読み取り専用で使ってみる
OverlayFS は、別のファイルシステムの上で動作する。 たとえば今回であれば ext4 で構築されたファイルシステム上で扱う。 なお、ドキュメントによると NFS 上で扱う場合には制限事項があるようだ。
$ df -T | head -n 3 Filesystem Type 1K-blocks Used Available Use% Mounted on /dev/root ext4 9983232 4428432 5538416 45% / devtmpfs devtmpfs 4068948 0 4068948 0% /dev
OverlayFS では、ファイルシステム上のディレクトリを重ねてマージした状態で扱うことができる。 これは、あまり言葉で説明しても分かりにくいと思うので、以下に例を示す。
まずは重ねるディレクトリとして lower1
と lower2
を用意する。
そして、マウントポイントとして merged
というディレクトリも用意する。
$ mkdir lower1 lower2 merged
lower1
と lower2
に、同じ名前を持った a
というファイルを用意しよう。
それぞれのファイルには、どちらのディレクトリ由来なのかがわかるようにテキストを書き込んでおく。
$ echo "lower1" > lower1/a $ echo "lower2" > lower2/a
また、lower2
にだけ存在するファイルとして b
も用意する。
$ echo "lower2" > lower2/b
上記の lower1
と lower2
を OverlayFS で重ね合わせて、merged
にマウントしてみよう。
このとき lowerdir
にコロン区切りでディレクトリを指定する。
$ sudo mount -t overlay overlay -o lowerdir=lower1:lower2 merged
マウント先を確認すると、merged
に a
と b
というファイルがある。
$ ls merged/ a b
ファイルの中身を確認すると、a
は lower1
のものが使われている。
ここから、lowerdir
は最初 (左側) に登場したディレクトリの内容が優先されることがわかる。
なお、lower2
にしか存在しない b
は、当然ながらそれ由来になる。
$ cat merged/a lower1 $ cat merged/b lower2
なお、lowerdir
だけを指定した場合には、ファイルシステムは読み取り専用になる。
これは、lowerdir
はファイルシステムの元ネタに過ぎないため、変更点を書き込む場所が存在しないため。
$ echo "Hello, World" > merged/c -bash: merged/c: Read-only file system
基本的な使い方が確認できたところでアンマウントしておこう。
$ sudo umount merged
アンマウントすると中身は空っぽに戻る。
$ ls merged/
OverlayFS を書き込める状態で使ってみる
続いては書き込み可能なファイルシステムを作ってみよう。
書き込み可能にする場合には workdir
と upperdir
というディレクトリを指定する必要がある。
まずはそれに使うディレクトリを作っておこう。
$ mkdir work upper
先ほどと同じ要領でマウントする。
ただし、今回はオプションに workdir
と upperdir
を指定する。
$ sudo mount -t overlay overlay -o lowerdir=lower1:lower2,workdir=work,upperdir=upper merged
すると、今度はマウントしたファイルシステムに書き込みが可能になる。
試しに c
というファイルを書き込んでみよう。
$ echo "Hello, World" > merged/c $ cat merged/c Hello, World
変更点は upperdir
に指定したディレクトリに書き込まれていることがわかる。
$ ls upper c $ cat upper/c Hello, World
なお、もちろんファイルを削除することもできる。
$ rm merged/b
削除されたファイルの情報はデバイス番号が 0/0
のキャラクタデバイスファイルとして表現される。
$ ls upper/ b c $ file upper/b upper/b: character special (0/0)
動作確認が終わったら、アンマウントして書き込まれた内容を掃除しておこう。
$ sudo umount merged $ rm -rf upper/*
Docker イメージを元に手動でコンテナもどきを作ってみる
さて、ここまでで OverlayFS の基本的な使い方がわかった。 次は Docker イメージを元に、OverlayFS を使って手動でコンテナもどきの環境を作ってみることにしよう。
まずは適当なコンテナイメージを取得する。
今回は python:3.9-slim
を使うことにした。
$ sudo docker image pull python:3.9-slim
試しに取得したイメージを使ってコンテナを立ち上げてみよう。
$ sudo docker container run --rm -it python:3.9-slim bash
次のように、このイメージでは Python 3.9 が利用できる。
# ls / bin dev home lib64 mnt proc run srv tmp var boot etc lib media opt root sbin sys usr # which python3 /usr/local/bin/python3 # python3 -V Python 3.9.7
ちなみに、今回使っているシステムにインストールされている Python はバージョンが 3.8 になっている。
$ python3 -V Python 3.8.10
さて、取得したイメージを docker inspect
で確認してみよう。
すると、イメージを構成しているディレクトリの情報が含まれることがわかる。
おや?この名称は OverlayFS で使われているものと似ていないだろうか。
$ sudo docker inspect python:3.9-slim | jq .[0].GraphDriver.Data { "LowerDir": "/var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff:/var/lib/docker/overlay2/9cea339dd2014f814c5b69087ba70aa62df4993f9aa74d6f4aacfd5fca5e156b/diff:/var/lib/docker/overlay2/ea324ac31f37d8a379fec3c132f2684d8928b11db6e83eb3922c93a14a674340/diff:/var/lib/docker/overlay2/4823ba02e2349749afa3a71b55c630b5a90decb0462fc136f1c2c246648ee540/diff", "MergedDir": "/var/lib/docker/overlay2/b5e26155a3241b7fc8df4497387d166687c09d3bdbf3ce56fe71899f209d6c87/merged", "UpperDir": "/var/lib/docker/overlay2/b5e26155a3241b7fc8df4497387d166687c09d3bdbf3ce56fe71899f209d6c87/diff", "WorkDir": "/var/lib/docker/overlay2/b5e26155a3241b7fc8df4497387d166687c09d3bdbf3ce56fe71899f209d6c87/work" }
LowerDir
に含まれるディレクトリを確認すると、これがイメージを構成している「レイヤー」の実体であることがわかる。
$ sudo find /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local/bin /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local/bin/pydoc /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local/bin/python-config /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local/bin/idle /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff/usr/local/bin/python $ sudo ls /var/lib/docker/overlay2/4823ba02e2349749afa3a71b55c630b5a90decb0462fc136f1c2c246648ee540/diff bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
なんとなく LowerDir
の中身は、そのまま OverlayFS のオプションとして渡せそうだ。
$ sudo docker inspect python:3.9-slim | jq -r .[0].GraphDriver.Data.LowerDir /var/lib/docker/overlay2/78920b19e12a8ada04d603b0c5565e8e30fc9139c929aa291e8e45118eb1fede/diff:/var/lib/docker/overlay2/9cea339dd2014f814c5b69087ba70aa62df4993f9aa74d6f4aacfd5fca5e156b/diff:/var/lib/docker/overlay2/ea324ac31f37d8a379fec3c132f2684d8928b11db6e83eb3922c93a14a674340/diff:/var/lib/docker/overlay2/4823ba02e2349749afa3a71b55c630b5a90decb0462fc136f1c2c246648ee540/diff
物は試しということで、先ほどと同じ要領で OverlayFS をマウントしてみよう。
$ sudo mount -t overlay overlay -o lowerdir=$(sudo docker inspect python:3.9-slim | jq -r .[0].GraphDriver.Data.LowerDir),workdir=work,upperdir=upper merged
何だか上手くいった感じがする。
$ ls merged/ bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
ここでおもむろに unshare(1) を使う。
unshare(1) は内部的に unshare(2) を呼んでプロセスのアイソレーションを操作できるコマンド。
以下で指定している -R
オプションは、プロセスのルートを指定したディレクトリに変更するというもの。
このとき、起動するプロセスとしては bash などのシェルを指定しよう。
$ sudo unshare -R merged bash
すると、OverlayFS のディレクトリをルートとして持ったシェルのプロセスが誕生する。
# ls /
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
このプロセスはシステムとは違って Python 3.9 が利用できる。
# which python3 /usr/local/bin/python3 # python3 -V Python 3.9.7 # python3 -c "print('Hello, World')" Hello, World
ということで、Docker イメージを元ネタに OverlayFS を直接使ってコンテナっぽいものを作ることができた。 もちろん、このプロセスはファイルシステム以外にはシステムとのアイソレーションができていない。 とはいえ、Docker (のコンテナランタイム) が内部的にやっていることは本質的に上記と同じこと。
いじょう。
参考
https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt