CUBE SUGAR CONTAINER

技術系のこと書きます。

Overlay Filesystem と Docker について

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 では、ファイルシステム上のディレクトリを重ねてマージした状態で扱うことができる。 これは、あまり言葉で説明しても分かりにくいと思うので、以下に例を示す。

まずは重ねるディレクトリとして lower1lower2 を用意する。 そして、マウントポイントとして merged というディレクトリも用意する。

$ mkdir lower1 lower2 merged

lower1lower2 に、同じ名前を持った a というファイルを用意しよう。 それぞれのファイルには、どちらのディレクトリ由来なのかがわかるようにテキストを書き込んでおく。

$ echo "lower1" > lower1/a
$ echo "lower2" > lower2/a

また、lower2 にだけ存在するファイルとして b も用意する。

$ echo "lower2" > lower2/b

上記の lower1lower2 を OverlayFS で重ね合わせて、merged にマウントしてみよう。 このとき lowerdir にコロン区切りでディレクトリを指定する。

$ sudo mount -t overlay overlay -o lowerdir=lower1:lower2 merged

マウント先を確認すると、mergedab というファイルがある。

$ ls merged/
a  b

ファイルの中身を確認すると、alower1 のものが使われている。 ここから、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 を書き込める状態で使ってみる

続いては書き込み可能なファイルシステムを作ってみよう。

書き込み可能にする場合には workdirupperdir というディレクトリを指定する必要がある。 まずはそれに使うディレクトリを作っておこう。

$ mkdir work upper

先ほどと同じ要領でマウントする。 ただし、今回はオプションに workdirupperdir を指定する。

$ 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