コンテナ隔離技術の基本原理
Namespaceはプロセスに対する「隔離」機能を提供し、Cgroupsはリソースに対する「制限」機能を提供します。これらを組み合わせることで、プロセスは独立した環境で実行される「サンドボックス」が実現されます。
しかし、重要な疑問が生じます。コンテナ内のプロセスから見えるファイルシステムはどのような状態なのでしょうか?
Mount Namespaceを使用すれば、コンテナプロセスは独立したファイルシステムビューを持つはずです。しかし実際には、単にMount Namespaceを有効にするだけでは、コンテナはホストのファイルシステムを継承してしまいます。
Mount Namespaceの検証
以下のCプログラムは、新しいプロセスを作成しMount Namespaceを有効化します:
#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char proc_stack[STACK_SIZE];
char* const proc_commands[] = {
"/bin/bash",
NULL
};
int child_process(void* arg)
{
printf("子プロセス - コンテナ内で実行中\n");
execv(proc_commands[0], proc_commands);
printf("エラーが発生しました\n");
return 1;
}
int main()
{
printf("親プロセス - コンテナを起動\n");
int child_pid = clone(child_process, proc_stack + STACK_SIZE,
CLONE_NEWNS | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
printf("親プロセス - コンテナが停止\n");
return 0;
}
このプログラムをコンパイルして実行すると:
$ gcc -o test_ns test_ns.c
$ ./test_ns
親プロセス - コンテナを起動
子プロセス - コンテナ内で実行中
コンテナ内でls /tmpを実行すると、ホストのファイルが表示されます。これはMount Namespaceが有効でも、ファイルシステムのマウントポイントが継承されているためです。
ファイルシステムの分離
コンテナプロセスが独立したファイルシステムビューを持つためには、明示的にマウント操作を行う必要があります:
int child_process(void* arg)
{
printf("子プロセス - コンテナ内で実行中\n");
// ルートファイルシステムをプライベートに設定
mount("", "/", NULL, MS_PRIVATE, "");
// /tmpをtmpfsで再マウント
mount("none", "/tmp", "tmpfs", 0, "");
execv(proc_commands[0], proc_commands);
printf("エラーが発生しました\n");
return 1;
}
この変更後、コンテナ内の/tmpディレクトリは空になります。マウント状態を確認すると:
$ mount -l | grep tmpfs
none on /tmp type tmpfs (rw,relatime)
重要な点は、このマウント操作がコンテナのMount Namespace内でのみ有効であり、ホストからは見えないことです。
ルートファイルシステムの変更
コンテナに完全に独立したファイルシステム環境を提供するには、ルートディレクトリ全体を再マウントする必要があります。chrootコマンドはこの目的に使用できます。
新しいルートファイルシステムの準備:
$ mkdir -p $HOME/container_root
$ mkdir -p $HOME/container_root/{bin,lib64,lib}
$ cp -v /bin/{bash,ls} $HOME/container_root/bin
依存ライブラリのコピー:
$ CONTAINER_ROOT=$HOME/container_root
$ lib_files="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for lib in $lib_files; do cp -v "$lib" "${CONTAINER_ROOT}${lib}"; done
chrootの実行:
$ chroot $HOME/container_root /bin/bash
これでプロセスは指定したディレクトリをルートファイルシステムとして認識します。この技術が発展し、Mount Namespaceが誕生しました。
コンテナイメージの概念
コンテナのルートディレクトリにマウントされ、隔離された実行環境を提供するファイルシステムを「コンテナイメージ」または「rootfs」と呼びます。
典型的なrootfsの構造:
$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
Dockerの核心技術は以下の3点です:
- Linux Namespaceの設定
- Cgroupsパラメータの設定
- プロセスのルートディレクトリ変更(pivot_rootまたはchroot)
rootfsにはオペレーティングシステムのカーネルは含まれていません。すべてのコンテナはホストOSのカーネルを共有します。
イメージのレイヤー構造
Dockerイメージは複数の「レイヤー」で構成されています。各レイヤーはrootfsに対する増分変更を表します。
Union File System(UnionFS)は、複数のディレクトリを単一のディレクトリに統合してマウントする技術です:
$ tree
.
├── dir_a
│ ├── file_a
│ └── common_file
└── dir_b
├── file_b
└── common_file
$ mkdir merged_dir
$ mount -t aufs -o dirs=./dir_a:./dir_b none ./merged_dir
$ tree ./merged_dir
./merged_dir
├── file_a
├── file_b
└── common_file
Dockerはこの技術を利用して、複数のレイヤーを統合した単一のファイルシステムビューを提供します。
Dockerのレイヤー構造詳細
Ubuntuイメージの例:
$ docker image inspect ubuntu:latest
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
コンテナのrootfsは3つの部分で構成されます:
1. 読み取り専用レイヤー
ベースイメージのレイヤーで、複数の読み取り専用層で構成されます。各層はOSの一部のファイルとディレクトリを含みます。
2. 読み書き可能レイヤー
最上位のレイヤーで、コンテナ内でのすべての変更がここに記録されます。ファイルの削除はwhiteoutファイル(.wh.ファイル名)で実現されます。
3. Initレイヤー
ホスト名やDNS設定など、コンテナ固有の設定ファイルを保持する特別な層です。docker commit時にはこの層は含まれません。
これらのレイヤーが統合され、コンテナに完全なファイルシステム環境を提供します。この構造により、イメージの効率的な共有とバージョン管理が可能になります。