エンジニアHubproduced by エン

若手Webエンジニアのための情報メディア

コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう

コンテナ技術を適切に活用するには、コンテナが「どうやって」動いているかを学びたいところ。はてなのエンジニアhayajo_77さんがコンテナの要素技術の勘所を解説します。

こんにちは。株式会社はてなでサーバー監視サービス「Mackerel」のSREを務めるhayajo_77@hayajoです。

さて、コンテナ技術はDockerの登場がきっかけとなり、本格的に活用が始まりました。現在はKubernetesを始めとするコンテナオーケストレーションツールや AWS, GCP, Azure などのクラウドサービスで提供されるコンテナマネジメントサービスを採用したサービス運用事例が数多く紹介されており、コンテナ技術は「理解する」フェイズから「利用する」フェイズに移ってきています。

コンテナそのものは上記のツールやサービスにより高度に抽象化されており、その要素技術に直接触れる機会はないかもしれません。ですが、効率の良い開発・運用をするためにはその仕組や特性を知ることはとても重要です。また、普及にともないgVisor, Kata Containers, Nabla Containers, Firecrackerなど、コンテナに対する新しいアプローチを持ったツールやサービスも出てきています。

これらのツールやサービスの特性、また「解決しようとしている問題」を理解し、自身が運用するサービスにマッチする技術を選択する上でも、コンテナ技術の基礎を理解することはとても大切です。

こうした背景から、本稿ではコンテナを構成する代表的な要素技術を解説します。コマンド例を多く載せているので、ぜひ手元で実行して理解を深めてください。本記事が少しでも開発・運用の一助となれば幸いです。

コンテナとは

コンテナはホストOS上の独立したアプリケーション実行環境です。独自のプロセステーブル、ユーザ、ファイルシステム、およびネットワークスタックを備えたフル機能のOS環境のように動作します。

仮想マシンとコンテナ

コンテナは一見すると仮想マシンとよく似ています。では、仮想マシンとの具体的な違いはどこにあるのでしょうか。簡単に両技術を整理してみます。

仮想マシンはハイパーバイザによって作成された仮想的なハードウェア環境です。仮想マシンにOS、さらに実行するアプリケーションやライブラリをインストールし、独立した実行環境を構築します。

一方でコンテナはコンテナランタイムによって作成された、ホストOSのリソースを隔離・制限したプロセスです。実行するアプリケーションやライブラリを個別に用意し、アプリケーション実行時にプロセスの属性を変更してホストOSのリソースを隔離・制限することで、独立した実行環境を構築します。

どちらも独立した実行環境を構築するものですが、これらは「オーバーヘッド」と「隔離レベル」について大きな違いがあります。

仮想マシンではアプリケーションやライブラリの他にOS実行のためのリソースが必要です。起動時間も数分かかる場合があります。 しかし、コンテナの場合はアプリケーションやライブラリの実行に必要なリソースだけを準備すればよく、起動時間は通常のアプリケーション実行と遜色ないレベルです。単純なアプリケーションであれば1秒もかからず起動します。つまり、コンテナは仮想マシンに比べてオーバーヘッドが小さく、ひとつのホストで多くのコンテナを実行することが可能なのです。

続いて、隔離レベルの違いについてです。 仮想マシンは仮想的なハードウェアレベルで隔離され、仮想マシンで実行されるアプリケーションはホストOSや他の仮想マシンから確認できません。また仮想マシンからホストOSや他の仮想マシンへのアクセス方法も限定的なため、セキュリティ面で問題があっても他の仮想マシンが受ける影響を最小限に留めることができます。

コンテナはホストOSのプロセスのひとつとして動作するので、ホストOSのプロセスリストからコンテナプロセスを確認可能です。また、ホストOSのリソースを共有することから、リソースの隔離・制限が不十分なコンテナがデプロイされた場合、ホストOSや他のコンテナに影響を与える可能性があります。コンテナは仮想マシンに比べて隔離レベルは低いので、運用には注意が必要となります。

先に挙げたgVisorやKata Containersといったコンテナランタイムでは隔離レベルが改善されているものもありますが、まずは仮想マシンとコンテナでこのような違いがあることを理解しておきましょう。

コンテナを作ってみよう

それでは、いくつかのコマンドを組み合わせて実際にコンテナを作ってみましょう。

動作環境

本記事において動作確認を行った環境はこちらとなります。

Vagrantfileはこちらとなります。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "1024"
    vb.cpus = 2
  end
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update

    apt-get install apt-transport-https ca-certificates curl software-properties-common jq
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
    apt-get update
    apt-get install -y docker-ce

    apt-get install -y cgdb
    apt-get install -y cgroup-tools

    apt-get install -y make gcc
    git clone git://git.kernel.org/pub/scm/linux/kernel/git/morgan/libcap.git /usr/src/libcap
    (cd /usr/src/libcap && make && make install)
  SHELL
end

仮想マシンなので失敗しても問題ありません。うまくいかないときは仮想マシンごと作り直して再度チャレンジしてみましょう。

コンテナを作る

動作環境の準備が整ったら早速コンテナを作ってみます。

最初にコンテナのルートファイルシステムを用意します。Dockerのbashイメージをテンポラリディレクトリに展開し、ここをコンテナのルートファイルシステムとします。

$ ROOTFS=$(mktemp -d)
$ CID=$(sudo docker container create bash)
$ sudo docker container export $CID | tar -x -C $ROOTFS
$ ln -s /usr/local/bin/bash $ROOTFS/bin/bash
$ sudo docker container rm $CID

続いてCPU、メモリを制限するグループ(cgroup)を作成します。ここでは仮にCPUを30%、メモリを10MBに制限してみます。

$ UUID=$(uuidgen)
$ sudo cgcreate -t $(id -un):$(id -gn) -a $(id -un):$(id -gn) -g cpu,memory:$UUID
$ cgset -r memory.limit_in_bytes=10000000 $UUID
$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=300000 $UUID

それではコンテナを作成します。 次のコマンドはcgroupでCPUとメモリを制限、Namespaceでカーネルリソースを隔離したコンテナを作成し、必要なファイルシステムをマウント、ホスト名を変更、ルートファイルシステムを変更、プロセスの権限を調整した上で /bin/sh を実行します。

$ CMD="/bin/sh"
$ cgexec -g cpu,memory:$UUID \
  unshare -muinpfr /bin/sh -c "
    mount -t proc proc $ROOTFS/proc &&
    touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty) &&
    touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx &&
    ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx &&
    touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null &&
    /bin/hostname $UUID &&
    exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
   "

コンテナ内でホスト名やユーザ、プロセステーブル、マウントテーブル、ネットワークなどを確認してみましょう。

# uname -n
# id
# ps aux
# mount
# ip link
# yes >/dev/null 

この状態でコマンドを実行したシェルとは別のシェルを開き、 ps コマンドでプロセスを確認すると、ホストOS上で unshare, /bin/sh, yes などのコンテナプロセスを以下のように確認できます。

$ ps f
  PID TTY      STAT   TIME COMMAND
 9268 pts/1    Ss     0:00 -bash
 9323 pts/1    R+     0:00  \_ ps f
 9165 pts/0    Ss     0:00 -bash
 9317 pts/0    S      0:00  \_ unshare -muinpfr ...
 9318 pts/0    S      0:00      \_ /bin/sh
 9321 pts/0    R+     0:01          \_ yes

また top コマンドなどで確認すると yes プロセス (PID=9321) のCPU利用時間が約30%に制限されていることが確認できます。

$ top -p 9321
top - 00:36:28 up  2:09,  2 users,  load avaerage: 1.16, 0.58, 0.24
Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.0 us,  0.3 sy,  0.1 ni, 98.4 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1008936 total,   105344 free,   146260 used,   757332 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   704796 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 9321 vagrant   20   0    1516      4      0 R   29.7  0.0   1:07.39 yes

ひととおり確認したら exit でコンテナを終了します。

# exit

コンテナ終了後は作成した cgroupとルートファイルシステムを削除して後片付けをしましょう。

$ sudo cgdelete -r -g cpu,memory:$UUID
$ rm -rf $ROOTFS

この例で「コンテナはホストOS上のプロセスではあるけれど、コンテナ内からは独立した環境に見える」ことが確認できたと思います。また、コンテナに対して使用できるリソースを制限できることも確認できました。

広く利用されているようなコンテナランタイムに比べて機能は不足していますが、ひとまずコンテナを作る雰囲気はつかめたのではないでしょうか。

コンテナを構成する要素技術

前章のコンテナ作成でいくつかのコマンドを組み合わせたように、コンテナは「コンテナ」と呼ばれる単一の技術でできているわけではありません。Namespaceとcgroupを中心に、下記に挙げるような複数の技術を組み合わせて構成されます。

  • Isolation: カーネルリソースやファイルシステムの隔離
    • Namespace
    • chroot/pivot_root
  • Limitation: ハードウェアリソースの制限
    • cgroup
    • rlimit
  • Restriction: 権限の制約
    • Capability
    • setuid/setgid
    • seccomp
    • MAC(SELinux, Apparmor など)

ここからは上記のコンテナを構成する要素技術の中から代表的なものを解説していきます。

Namespace

Namespaceはプロセスが参照するPID(プロセスID)番号空間やマウントポイントなど、カーネルリソースを他のプロセスと隔離し、独立したOS環境のように見せる機能です。

Namespaceはすべてのプロセスに関連付けられていて、指定がない限り親プロセスと同じNamespaceを参照します。新しくNamespaceが作成されていない環境において、全てのプロセスはPID1と同じNamespaceを参照し、プロセス間で共通のカーネルリソースを扱います。

このため他のプロセスや使うことのないライブラリなど、自身のプロセスに必要のないリソースまで参照できてしまいます。さらに、あるプロセスの操作が他のプロセスに影響を与えてしまう場合もあります。

プロセスに新しくNamespaceを関連付けることで、参照・操作可能なリソースを制限できます。

なお、現在ではNamespaceには7つの種類があり、それぞれ隔離できるリソースは異なります。7種の特徴を簡単に説明すると、以下のようになります。

  • Mount Namespace: マウントポイントを隔離してプロセス独自のファイルシステムを扱えるようにします。
  • PID Namespace: PID番号空間を隔離してユニークなPIDを持ちます。新しいNamespaceで最初に作成されたプロセスはPID1となり、通常のPID1プロセスと同様の特性を持ちます。
  • Network Namespace: ネットワークスタックを隔離します(後に解説あり)。
  • IPC Namespace: SysV IPCオブジェクト、 POSIXキューを隔離します。
  • UTS Namespace: ホスト名やNISドメイン名など、 unameシステムコールで返される情報を隔離します。
  • User Namespace: User ID, Group IDを隔離します。Namespace内ではUser IDが0で特権ユーザーである一方、他のNamespaceからは非特権ユーザーとして扱われる、という状態を持つことができます。
  • Cgroup Namespace: cgroupルートディレクトリを隔離します。新しくCgroup Namespaceを作成すると現在のcgroupディレクトリがcgroupルートディレクトリになります。

Namespaceの操作方法

Namespaceは以下のようなシステムコールやそれをラップしたコマンドで操作します。

  • unshareシステムコール: 現在のプロセスを新しいNamespaceに関連付けます。
  • setnsシステムコール: 現在のプロセスを既存のNamespaceに関連付けます。
  • cloneシステムコール: 新しくプロセスを作成する際に、そのプロセスを新しいNamespaceに関連付けます。
  • util-linuxパッケージ
    • unshareコマンド: 新しいNamespaceを作成し、指定したコマンドを実行します。
    • nsenterコマンド: 既存のNamespaceに関連付けて指定したコマンドを実行します。
  • iproute2パッケージ
    • ip netnsコマンド: Network Namespaceを管理します。

Namespaceを確認する

プロセスのNamespaceは /proc/<PID>/ns で確認できます。

$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 net -> 'net:[4026531993]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 user -> 'user:[4026531837]'
lrwxrwxrwx 1 vagrant vagrant 0 Jan 18 10:54 uts -> 'uts:[4026531838]'

ここにある各ファイル(シンボリックリンク)はNamespaceの種別とinode番号で構成される文字列です。この文字列が同じプロセス同士はNamespaceを共有しています。

現在のプロセスのNamespaceとPID1のNamespaceを比較して、同じNamespaceを参照していることを確認してみてください。

Namespaceを隔離する

それではNamespaceでカーネルリソースを隔離してみましょう。以下の例ではunshareコマンドでMount, UTS, IPC, PID, User Namespaceを隔離して /bin/sh を実行します。指定した各リソースが隔離され、プロセス独自の環境になっていることが確認できます。

$ unshare --mount-proc -uipr --fork /bin/sh
# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.4  23148  5012 pts/0    S    06:56   0:00 /bin/bash
root        11  0.0  0.3  37792  3192 pts/0    R+   06:56   0:00 ps aux
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# exit

Namespaceに接続する

nsenterコマンドで実行中のプロセスのNamespaceに接続して指定したコマンド実行できます。 コマンド内部では現在のプロセスを既存のNamespaceに関連付けるsetns(2)を実行しています。

次の例ではUTS Namespaceを隔離したプロセスでホスト名を"foobar"に変更し、nsenterで当該プロセスのUTS Namespaceに関連付けてホスト名を参照します。

$ unshare -ur /bin/sh -c 'hostname foobar; sleep 15' &
$ PID=$(jobs -p)
$ sudo nsenter -u -t $PID hostname
foobar

Dockerではdocker execコマンドが実行中のコンテナに接続するコマンドとして広く使われていますが、こちらも実行中のコンテナ(プロセス)のNamespaceに関連付けて、指定したコマンドを実行するものです。

Namespaceを維持する

Namespaceは関連するすべてのプロセスが終了すると消えてしまいます。ですが /proc/PID/ns 以下のファイルをbind mountすることでこれを維持することができます。

unshareコマンドではオプションにマウント先を指定することでbind mount可能です。

$ touch ns_uts
$ sudo unshare --uts=ns_uts /bin/sh -c 'hostname foobar'

こちらの例では前の例と違いUTS Namespaceを隔離したプロセスは終了していますが、UTS Namespaceは ns_uts ファイルにbind mountされて維持されています。

$ mount | grep ns_uts
nsfs on /home/vagrant/ns_uts type nsfs (rw)

nsenterでこのファイルを指定すると以前のNamespaceをプロセスに関連付けることができます。

$ sudo nsenter --uts=ns_uts hostname
foobar

このように、Namespaceを利用することで仮想化インスタンスを追加することなく、カーネルリソースの隔離できます。

cgroup

cgroupはグループ化したプロセスに対してカーネルリソースやハードウェアリソースを制限することができる機能です。

ホストのすべてのプロセスはCPUやメモリを共有します。あるプロセスが多くのCPUやメモリを消費してしまうと、新しくアプリケーションを実行できなくなったり動作が遅く不安定になったり、最悪の場合、他のプロセスが勝手に終了されてしまいます。cgroupではプロセスが利用できるリソースを制限し、こうしたリスクを抑制できます。cgroupの機能は具体的に表現すると以下のようになります。

  • CPU時間の制限、割り当てCPUの指定
  • メモリ使用量の制限、OOM killerの有効化/無効化
  • プロセス数の確認と制限
  • デバイスのアクセス制御
  • ネットワーク優先度設定
  • タスクの一時停止/再開
  • CPU使用量、メモリ使用量のレポート
  • ネットワークパケットをタグ付け(tc で利用)

こうした機能を持つことから、Namespaceと組み合わせることでプロセスを仮想マシンのように扱うことができます。

cgroupfs

cgroupはcgroupfsというVFS(Virtual File System)をマウントし、ディレクトリによる階層構造でグループを表現します。v1, v2がありますが、ここではv1をベースに解説します。

cgroupは一般的に /sys/fs/cgroup//cgroup にマウントされます。

$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

cpu, memoryなどは、それぞれサブシステムまたはコントローラと呼ばれます。サブシステムは"cpu,cpuacct"のように複数でまとめることもできます。

またサブシステムごとに複数の階層を持つことができます。AWS ECSではcpuやmemoryサブシステムに ecs/<TASK_ID>/<CONTAINER_ID> の階層を作り、タスクやコンテナ単位でリソースを制限しています。

$ tree -d /cgroup/cpu
/cgroup/cpu
[...]
└── ecs
    [...]
    └── 00354727-e078-4e21-a9be-cea13649aebe
        ├── 82cfe943...
        └── fd67279a...

cgroupの操作方法

cgroupはファイルシステムの直接操作や、これをラップした以下のコマンドで操作します。Namespaceと異なりシステムコールでは操作しません。

  • cgroupfsに対するファイル操作
  • cgroup-tools(libcgroup)パッケージ
    • cgcreateコマンド: 新しくサブグループを作成します。
    • cgdeleteコマンド: 指定したサブグループを削除します。
    • cgexecコマンド: 指定したサブグループでコマンドを実行します。

cgroupを確認する

プロセスが属するグループは /proc/<PID>/cgroup で確認できます。

次の例ではcpu, memoryサブシステムに新しくサブグループをつくり、コマンド実行時にこれを指定しています。それ以外のサブシステムは自動的にrootグループとなります。

$ UUID=$(uuidgen)
$ sudo cgcreate -g cpu,memory:$UUID
$ sudo cgexec -g cpu,memory:$UUID cat /proc/self/cgroup
12:freezer:/
11:pids:/user.slice/user-1000.slice/session-116.scope
10:memory:/25ac9868-6e6b-4a6b-ab31-06e046b7947f
9:devices:/user.slice
8:perf_event:/
7:cpuset:/
6:net_cls,net_prio:/
5:blkio:/user.slice
4:rdma:/
3:cpu,cpuacct:/25ac9868-6e6b-4a6b-ab31-06e046b7947f
2:hugetlb:/
1:name=systemd:/user.slice/user-1000.slice/session-116.scope
0::/user.slice/user-1000.slice/session-116.scope

cgroupのグループに属するプロセスは <SUBSYSTEM>/[<SUBGROUP>]/cgroup.procs で確認できます。先程作成したサブグループに属するプロセスは次のようにして確認できます。

$ sudo cgexec -g cpu:$UUID sleep 10 &
$ cat /sys/fs/cgroup/cpu/$UUID/cgroup.procs
1914

cgroupでリソースを制限する

こちらの例ではcgroup-toolsではなくcgroupfsを直接操作してサブグループを作成し、CPUを制限しています。プロセスをサブグループに属するよう設定するには<SUBSYSTEM>/<SUBGROUP>/cgroup.procs にPIDを書き込みます。

$ UUID=$(uuidgen)
$ sudo mkdir /sys/fs/cgroup/cpu/$UUID
$ sudo echo 50000 | sudo tee /sys/fs/cgroup/cpu/$UUID/cpu.cfs_quota_us
$ echo $$ | sudo tee /sys/fs/cgroup/cpu/$UUID/cgroup.procs
$ timeout 15s yes >/dev/null &
$ top # yesコマンドのCPU利用量が50%前後で頭打ちになっていることが確認できます

Capability

Capabilityはroot権限を細分化してプロセスやファイルに設定する機能です。特権(root)ユーザで動作するプロセスは全ての権限を持つため、実行しているプログラムに脆弱性があった場合、他のプロセスやホストOSそのものに影響を与えてしまいます。

Capabilityを操作してプロセスに必要な権限だけを許可することにより、その影響範囲を狭めることができます。

SUID rootとCapability

一般的に非特権ユーザはRAWソケットを扱うことができません。ですが /bin/ping はSUID(Set User ID)rootにより特権ユーザで動作するので、非特権ユーザでもRAWソケットを扱うことができます。

$ ls -l /bin/ping # ファイルのオーナーはrootでSUIDが設定されています
-rwsr-xr-x 1 root root 64424 Mar  9  2017 /bin/ping

$ ping -c1 127.0.0.1 # このプロセスは特権ユーザで動作します
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.039/0.039/0.039/0.000 ms

ただし、このプロセスは特権ユーザとして動作するためRAWソケットを扱う以上の権限を持っています。もしこのプログラムに脆弱性があった場合にはホストOSを自由に操作されてしまう恐れがあります。Capabilityを設定して実行するプログラムの動作に必要な権限だけを与えることで、脆弱性があった場合でもその影響を最小限にできます。

次の例はコピーした /bin/ping にRAWソケットを扱う権限(CAP_NET_RAW)だけを与えます。コピーしたpingのオーナーは非特権ユーザとなります。そのままでは権限がないため実行時にエラーとなります。

$ cp /bin/ping .
$ ls -l ping # コピーしたファイルのオーナーは非特権ユーザとなります
-rwxr-xr-x 1 vagrant vagrant 64424 Jan 21 04:52 ping

$ ./ping -c1 -q 127.0.0.1 # 特権ユーザで動作しないためRAWソケットが扱えません
ping: socket: Operation not permitted

$ sudo setcap CAP_NET_RAW+ep ./ping # コピーしたpingにRAWソケットを扱う権限を与えます
$ ./ping -c1 -q 127.0.0.1 # 非特権ユーザで動作しますがRAWソケットを扱えます
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.030/0.033/0.036/0.000 ms

Capabilityの種類

現在定義されているCapabilityは37種類あります。

実行するプログラムに必要なCapabilityはstraceコマンドなどで調べる必要があります。

DockerのデフォルトCapability

DockerにおけるコンテナのデフォルトCapabilityは以下のコマンドで調べることができます。

$ capsh --decode=$(cat /proc/$(sudo docker container inspect $(sudo docker run --rm -d busybox sleep 10) -f '{{.State.Pid}}')/status | awk '/^CapEff:/{print $2}')
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap

--privilegedフラグを指定した実行したコンテナではすべてのCapabilityが有効になることを確認できます。

$ capsh --decode=$(cat /proc/$(sudo docker container inspect $(sudo docker run --privileged --rm -d busybox sleep 10) -f '{{.State.Pid}}')/status | awk '/^CapEff:/{print $2}')
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read

Capability Set

CapabilityはCapability Setという形でプロセスとファイル、それぞれに設定します。Capability Setには以下の種類があります。

  • Permitted: EffectiveとInheritableで持つことが許可されるCapability Setです。一度OFFにしたものは自力で再セットできません。
  • Inheritable:execve(2)した際に継承するCapability Setです。
  • Effective: 実際に判定されるCapability Setです。(ファイルに設定する場合はON/OFFの1ビット)
  • Ambient: 特権のないプログラムをexecve(2)した際に保持されるCapability Setです。
  • Bounding Set: 取得できるCapabilityを制限するための集合です。

最終的にプロセスが持つCapability Setは次の計算式で決定されます。

P'(ambient) = (file is privileged) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable))
          | (F(permitted) & cap_bset)
          | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)

* P: execve(2)前のプロセスのCapability Set
* P': execve(2)後のプロセスのCapability Set
* F: ファイルのCapability Set
* cap_bset: プロセスのバウンディングセット

計算はこのようにとても複雑です。理解を深めるためには以下のエントリに目を通すことをおすすめします。

Capabilityの操作方法

Capabilityは以下のシステムコールやそれらをラップしたコマンドで操作します。

  • prctl, capget, capsetシステムコール: プロセスのCapability Setを操作します。
  • setxattr, getxattrシステムコール: ファイルの拡張属性を操作してCapability Setを扱います。
  • attrパッケージ
    • attrコマンド: ファイルの拡張属性を扱います。
    • getfattrコマンド: ファイルの拡張属性を表示します。
    • setfattrコマンド: ファイルの拡張属性を設定します。
  • libcapパッケージ
    • capshコマンド: Capability設定をサポートした /bin/bash のラッパーです。
    • getcapコマンド: ファイルのCapability Setを表示します。
    • setcapコマンド: ファイルのCapability Setを設定します。
    • getpcapコマンド: 指定されたプロセスのCapability Setを表示します。

Capabilityの確認

プロセスのCapability Setは /proc/<PID>/status で確認できます。以下は特権ユーザ(root)の場合のCapability Setです。Effective Capability Set(CapEff)がすべて有効になっていることが確認できます。

$ sudo cat /proc/self/status | grep ^Cap
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

続いて非特権ユーザの場合のCapability Setは以下のようになります。Effective Capability Set(CapEff)がすべて無効になっており、特権がないことが確認できます。

$ cat /proc/self/status | grep ^Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

また、ファイルのCapability Setはgetcapコマンドやgetfattrコマンドで確認できます。先程コピーしたpingのCapalibity Setをgetcapコマンドで確認する例です。

$ getcap ./ping
./ping = cap_net_raw+ep

プロセスにCapabilityを設定する

前述の解説ではコピーしたpingファイルにCapability Setを設定して特権を与えました。しかし、ここから解説する例ではAmbient Capability Setを設定することで、ファイルにCapability Setを設定することなくコピーしたpingを実行できます。

動作環境のlibcap-2.25付属のcapshコマンドはAmbient Capabilityに対応していないため、gitから最新のものをcloneしてインストールしています。

以下が実行例ですが、capshのオプションは指定した順に処理されるので注意してください。 cap_setpcapは--addambオプションの実行に、cap_setuidとcapsetgidは--userオプションの実行に必要となるCapabilityです。これらの特権はcapshでの前処理だけで必要な特権なのでInheritable Capability Setを設定する必要はありません。

$ cp /bin/ping .
$ sudo capsh \
        --caps="cap_net_raw+ip cap_setpcap,cap_setuid,cap_setgid+ep" \
        --keep=1 --user=$USER --addamb=cap_net_raw -- \
        -c 'id && getcap -v ./ping && grep Cap /proc/$$/status && ./ping -c1 127.0.0.1'
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)
./ping
CapInh: 0000000000002000
CapPrm: 0000000000002000
CapEff: 0000000000002000
CapBnd: 0000003fffffffff
CapAmb: 0000000000002000
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.074 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.074/0.074/0.074/0.000 ms

実行される /bin/bash プロセス(capsh内から実行される)のAmbient Capability SetにはCAP_NET_RAWが設定されるため、このプロセスの子プロセスではRAWソケットを扱うことができます。このためコピーしたpingファイルにCAP_NET_RAWを与える必要はありません。

chrootとpivot_root

chroot, pivot_rootはプロセスのルートファイルシステムを隔離する目的で使用します。Mount Namespaceとあわせて使うことで他のプロセスと隔離された専用のファイルシステムを持つことができます。

ファイルシステムを隔離することでホストOSのファイルシステムへのアクセスを制限でき、よりセキュアな状態を作れます。

chroot, pivot_rootはそれぞれ操作の対象に違いがあります。

  • chroot: プロセスのルートディレクトリが指すパスを変更します。アクセス可能な範囲を限定させることでファイルシステムを隔離します。
  • pivot_root: プロセスのルートファイルシステムそのものを入れ替えることでファイルシステムを隔離します。

chroot, pivot_rootの操作

chroot, pivot_rootはシステムコールやそれをラップしたコマンドで操作します。

  • chrootシステムコール, chrootコマンド
  • pivot_rootシステムコール, pivot_rootコマンド

chroot, pivot_rootの確認

chroot, pivot_rootはそれぞれ /proc で確認できます。次の例では現在のプロセスのルートディレクトリは / 、ルートファイルシステムはext4でフォーマットされている/dev/sda1/ であることが確認できます。

$ ls -l /proc/$$/root
lrwxrwxrwx 1 vagrant vagrant 0 Jan 21 05:32 /proc/2919/root -> /
$ awk '{ if ($5 == "/") print $0 }' /proc/$$/mountinfo
26 0 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered

chrootでファイルシステムを隔離する

プロセスのルートディレクトリは通常 / ですが、chrootではこれの指す先を変更することでファイルシステムを隔離します。

次の例では適当なディレクトリにバイナリとライブラリをコピーし、そのディレクトリをchrootでルートディレクトリに変更して /bin/sh を実行します。このプロセスではchrootしたディレクトリ(とその配下)以外にはアクセスできません。

$ ROOTFS=$(mktemp -d) # ルートファイルシステムを用意します
$ cp -a /bin /lib /lib64 $ROOTFS

$ sudo chroot $ROOTFS /bin/sh # chrootでルートディレクトリを変更します
# ls -l /
total 12
drwxr-xr-x  2 1000 1000 4096 Sep 27 19:22 bin
drwxr-xr-x 21 1000 1000 4096 Oct 17 01:04 lib
drwxr-xr-x  2 1000 1000 4096 Oct 17 01:06 lib64

# cat /etc/passwd # ホストのファイルにはアクセスできません
cat: /etc/passwd: No such file or directory

chrootからの脱出

先に「chrootしたディレクトリ以外にはアクセスできない」と説明しましたが、実はCAP_SYS_CHROOT特権が有効(chrootシステムコールが実行可能)なプロセスであれば簡単に抜け出せてしまうのです。

次のコードはchrootを脱出するプログラムです。なお、エラーチェックは省略しています。

$ cat <<EOF > escape-chroot.c # 脱出プログラムを用意します
#include <stdio.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
  int i;
  mkdir(".dummy", 0755);
  chroot(".dummy");
  for (i = 0; i < 256; i++) {
    chdir("..");
  }
  if (chroot(".") < 0) {
    fprintf(stderr, "chroot failed: %s\n", strerror(errno));
    return 1;
  }
  argv++;
  execvp(argv[0], argv);
  fprintf(stderr, "%s failed: %s\n", argv[0], strerror(errno));
  return 0;
}

EOF
$ gcc -Wall -o escape-chroot escape-chroot.c

$ ROOTFS=$(mktemp -d) # chrootする環境に脱出プログラムもコピーします
$ cp -a /bin /lib /lib64 escape-chroot $ROOTFS

$ sudo chroot $ROOTFS /bin/sh # chrootしてルートディレクトリを変更します

# cat /etc/passwd # 通常はホストのファイルにはアクセスできません
cat: /etc/passwd: No such file or directory

# ./escape-chroot cat /etc/passwd" # 脱出プログラムを介すことでホストのファイルにアクセスできてしまいます
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin 
[...]

chrootシステムコールでは、プロセスのルートディレクトリを変更します。これは絶対パスの探索起点になります。

一方でchdir(2)はプロセスのカレントディレクトリを変更し、これは相対パスの探索起点となります。

chroot(2)だけを実行した場合、プロセスのカレントディレクトリはchrootしたディレクトリの外側のまま変更されません。この状態では相対パスを使って自由にディレクトリを移動できてしまいます。ただし一度でもchrootで指定したディレクトリ配下に移動すると制限されます。

chroot環境からの脱出を防ぐためには、非特権ユーザーへの変更やCAP_SYS_CHROOT権限を無効にするなど、chrootを実行させないための対策が必要となります。

以下の例はcapshコマンドでCAP_SYS_CHROOT権限を無効してchrootの実行を抑制しています。

$ sudo capsh --chroot=$ROOTFS --drop=cap_sys_chroot --
$ ./escape-chroot cat /etc/passwd
chroot failed: Operation not permitted

pivot_rootでルートファイルシステムを入れ替える

pivot_rootはプロセスのルートファイルシステムを入れ替えることで他のプロセスとファイルシステムを隔離します。

pivot_rootシステムコールの実行には、以下のような複数の条件を満たす必要があります。

  • 新しいファイルシステム(new_root)と元のファイルシステムの移動先(put_old)はディレクトリでなければならない
  • new_rootとput_oldは現在のrootと同じファイルシステムにあってはならない
  • put_oldはnew_root配下になければならない
  • 他のファイルシステムがput_oldにマウントされていてはならない

少々ややこしい印象ですが、より詳しい内容については以下のエントリも参照してください。

そして、以下の例では先程のchrootの例で作成した$ROOTFSにpivot_rootしてルートファイルシステムを入れ替えています。すでにディレクトリ削除してしまった場合は再作成して試してみてください。

$ NEW_ROOT=$ROOTFS # pivot_rootするために必要な準備をします
$ mkdir $NEW_ROOT/{.put_old,proc}

$ unshare -mpfr /bin/sh -c " \ # ルートファイルシステムをprivateマウントにします
  mount --bind $NEW_ROOT $NEW_ROOT && \ # bind-mountしてnew_rootを別のファイルシステムにします
  mount -t proc proc $NEW_ROOT/proc && \ # put_oldをアンマウントするためにprocfsをマウントしてマウントポイントを参照できるようにします
  pivot_root $NEW_ROOT $NEW_ROOT/.put_old && \ # pivot_rootでルートファイルシステムを入れ替えます
  umount -l /.put_old && \ # 元のルートファイルシステムをアンマウントします
  cd / && \ # プロセスのカレントディレクトリを変更します
  exec /bin/sh # プロセスイメージを入れ替えます
"

# ls -l /
total 24
drwxr-xr-x   2     0     0 4096 Sep 27 19:22 bin
-rwxrwxr-x   1     0     0 8632 Jan 28 03:08 escape-chroot
drwxr-xr-x  21     0     0 4096 Jan 28 01:52 lib
drwxr-xr-x   2     0     0 4096 Jan 28 01:52 lib64
dr-xr-xr-x 113 65534 65534    0 Jan 28 03:13 proc


# cat /etc/passwd
cat: /etc/passwd: No such file or directory
# ./escape-chroot cat /etc/passwd # ファイルシステムが入れ替わっているのでファイルが存在しません
cat: /etc/passwd: No such file or directory

pivot_rootした後はルートファイルシステムそのものが変更されているので、chrootの例のように脱出することはできません。pivot_rootを利用するための条件は複雑ですが、chrootよりもセキュアにファイルシステムを隔離できるのです。

Copy On Write(COW)ファイルシステム

コンテナを作る際にchroot, pivot_rootで指定するファイルシステムですが、ここまで解説してきた例では、コンテナ作成ごとにホストOSからコピーしたりDockerイメージから展開する必要がありました。

同じホストで同じ構成のコンテナを複数作成する場合、各コンテナ同士では多くの共有可能なバイナリやライブラリがあります。コンテナごとにファイルシステムをまるごと用意する、とはディスクスペースを無駄に消費していることと同義なのです。また、大きな環境の場合、ファイルシステムの作成にも時間がかかりコンテナの起動が遅くなってしまう可能性もあります。こうした無駄を改善するのが、COW(Copy On Write)ファイルシステムです。

COWとは、ベースとなるファイルシステムを読み取り専用とし、ここに含まれるファイルに対する書き込みが発生したときに該当のファイルをコピーして書き込みを行う、という仕組みを持ったファイルシステムです。仮想メモリでもおなじみの機能ですね。

COWファイルシステムでは複数のコンテナが共通で参照するファイルシステムはひとつあれば十分で、コンテナごとにコピーする必要はありません。このためディスクスペースの節約やファイルシステムの作成によるコンテナの起動時間を改善できます。さらに、プロセスをまたいだページキャッシュの共有による性能向上も期待できます。

このように、多くの効果が望める一方、ファイルの一部分の変更でもファイルコピーが発生します。ベースファイルシステム内のファイル変更が頻繁に行われるようなケースでは、書き込み性能に注意が必要になります。Dockerではこうした頻繁なファイル変更が必要な場合、ボリュームコンテナの利用が推奨されます。

以下は代表的なCOWファイルシステムです。

  • Unioning Filesystem
    • AUFS, OverlayFS
  • Snapshotting Filesystem
    • Btrfs, ZFS
  • Copy-On-Write Block Device
    • Device mapper

本稿ではDockerでも広く利用されているOverlayFSを紹介します。

OverlayFS

OverlayFSは2つのディレクトリを重ね合わせ、ひとつのディレクトリとして見せるファイルシステムで、以下のディレクトリで構成されます。

  • lowerdir: 下位に位置するディレクトリでファイルシステムのベースとなるディレクトリです。このディレクトリは読み取り専用です。
  • upperdir: 上位に位置するディレクトリで、lowerdirに対して新規作成、変更、削除されたファイルが書き出されるディレクトリです。
  • merged: lowerdirとupperdirが結合されたディレクトリです。OverlayFSをマウントしたプロセスでは、作業者が閲覧するのはこのディレクトリとなります。
  • work: OverlayFSの内部で使用される作業用ディレクトリです。

OverlayFSは通常のファイルシステム同様、mountシステムコールやmountコマンドで操作します。 以下はmountコマンドでOverlayFSを利用する例です。

sudo mount 
   -t overlay \
   -o lowerdir=/path/to/base,upperdir=/path/to/upper/diff,workdir=/path/to/upper/work \
   overlay \
   /path/to/upper/merged

OverlayFSを利用したコンテナイメージの作成

Dockerコンテナを作成する際に、そのベースファイルシステムとなるDockerイメージはOverlayFSのようにレイヤを重ね合わせた形で提供されています。ベースとなるOSレイヤ、そこにアプリケーションに必要なツールやライブラリをインストールしたレイヤ、さらにアプリケーションをインストールしたレイヤなど、複数のレイヤを重ね合わせてDockerイメージとなります。

ここから紹介する例ではOverlayFSで以下のDockerfileで表すようなコンテナイメージを作り、そのイメージをルートファイルシステムとしてアプリケーションを実行します。Dockerイメージがどのようにしてできているのか、雰囲気を掴めるのではないかと思います。

# LAYER1
FROM alpine:latest

# LAYER2
RUN apk add --no-cache curl

ENTRYPOINT ["curl"]

まずはベースとなるLAYER1を作ります。ここはDockerのalpine:latestイメージを展開して作成します。

$ LAYER1=$(mktemp -d)
$ mkdir $LAYER1/diff
$ CID=$(sudo docker container create alpine:latest)
$ sudo docker export $CID | tar -x -C $LAYER1/diff
$ sudo docker rm $CID

続いてLAYER2です。LAYER1をOverlayFSのlowerdirとしてマウントし、curlをインストールします。このレイヤにはcurlのインストールで作成された差分ファイルのみが保存されます。

$ LAYER2=$(mktemp -d)
$ mkdir $LAYER2/{diff,work,merged}
$ sudo mount \
   -t overlay \
   -o lowerdir=$LAYER1/diff,upperdir=$LAYER2/diff,workdir=$LAYER2/work \
   overlay \
   $LAYER2/merged
$ sudo mount --bind /etc/resolv.conf $LAYER2/merged/etc/resolv.conf
$ unshare -r chroot $LAYER2/merged apk add --no-cache curl
$ sudo umount -R $LAYER2/merged
$ rm -rf $LAYER2/{work,merged}

作成したレイヤ(LAYER1, LAYER2)を組み合わせたものがコンテナイメージとなります。下記のようにLAYER1, LAYER2をOverlayFSのlowerdirとしてコンテナイメージを用意し、これをルートファイルシステムとしてコンテナを作成し、アプリケーションを実行します。

$ CONTAINER=$(mktemp -d)
$ mkdir $CONTAINER/{diff,work,merged}
$ ROOTFS=$CONTAINER/merged
$ sudo mount -t overlay \
   -o lowerdir=$LAYER2/diff:$LAYER1/diff,upperdir=$CONTAINER/diff,workdir=$CONTAINER/work \
   overlay \
   $ROOTFS
$ sudo mount --bind /etc/resolv.conf $ROOTFS/etc/resolv.conf
$ ARGS="http://httpbin.org/uuid"
$ unshare \
   -uipr \
   --mount-proc \
   --fork \
   chroot $ROOTFS /bin/sh -c "mount -t proc proc /proc && exec curl $ARGS"

OverlayFSを利用しても、コンテナごとにルートファイルシステムを用意する必要はありますが、ベースとなるコンテナイメージさえあればコンテナ内に含まれるファイルを“全て”用意コピーする必要はないため、ディスクスペースに無駄がなく起動時間も改善されます。

ネットワークリソースの隔離

通常であればネットワークインターフェイスやルーティングテーブル、ソケットなどのネットワークリソースはOS全体で共有されます。コンテナごとにこれらを独立して扱えるようにするには、前述したNamespaceの1つであるNetwork Namespaceを隔離する必要があります。 Network Namespaceは以下に挙げるネットワークリソースを隔離して、独立したネットワークスタックを提供します。

  • ネットワークデバイス
  • IPv4/v6プロトコルスタック
  • ルーティングテーブル
  • ファイヤーウォールルール
  • ソケット

また、隔離されたNetwork Namespaceには専用のループバックデバイスが提供されます。

$ unshare -nr ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

ネットワークインターフェースは1つのNetwork Namespaceにしか存在できないため、隔離したNamespaceではroot Namespace(ホストOSのPID1のNamespace)が持つネットワークインターフェースを利用できません。隔離したNetwork Namespaceが他のNamespaceや外部ネットワークと通信するためには、このNamespaceにネットワークインターフェースを割り当て、適切に設定する必要があります。

Dockerではvethペアを利用し、一つをroot Namespaceに作成したネットワークブリッジに、もう一つを隔離したNamespaceに割り当て、Namespace間で通信できるようにします。IPアドレス、ルーティングルールを設定しNATを有効にすることで、Namespaceが隔離されたコンテナでも外部ネットワークの通信が可能になります。

ただしこの構成ではroot Namespaceと隔離されたNamespaceの2つのネットワークスタックでパケットが処理されることになるので、ネットワークのパフォーマンスには気を配る必要があるでしょう。

Network Namespaceを使ったプライベートネットワークの作成

以下に紹介するのはNetwork Namespaceを隔離し、ネットワークブリッジ、vethを組み合わせてプライベートネットワークを作成する例です。

まずはネットワークブリッジ(br0)を作成します。

$ sudo ip link add name br0 type bridge
$ sudo ip addr add 192.168.77.1/24 broadcast 192.168.77.255 label br0 dev br0
$ sudo ip link set dev br0 up

次にip netnsコマンドでNetwork Namespace(ns0)を作成します。

$ sudo ip netns add ns0

vethペア(veth0, veth0_peer)を作成し、片方のmasterをネットワークブリッジ(br0)に追加、もう片方を隔離したNetwork Namespace(ns0)に移動します。

$ sudo ip link add veth0 type veth peer name veth0_peer
$ sudo ip link set dev veth0 master br0
$ sudo ip link set dev veth0 up
$ sudo ip link set dev veth0_peer netns ns0

ns0のveth(veth0_peer -> eth0)にアドレスを割り当て、デフォルトゲートウェイを設定します。

$ sudo ip netns exec ns0 ip link set dev veth0_peer name eth0
$ sudo ip netns exec ns0 ip addr add 192.168.77.2/24 dev eth0
$ sudo ip netns exec ns0 ip link set dev eth0 up
$ sudo ip netns exec ns0 ip route add default via 192.168.77.1

これでネットワークブリッジ(br0)と隔離したNamespace(ns0)のvethが通信できるようになりました。ip netns execコマンドで隔離したNamespaceを参照するコンテナを作成し、br0(192.168.77.1)やホストアドレスにpingして疎通を確認します。

$ sudo ip netns exec ns0 ping -c 3 192.168.77.1

コンテナ内から外部ネットワークと通信する場合はホスト側で適宜NATを設定してください。 最後に後片付けをしておきましょう。ns0を削除するとvethペアは自動的に削除されます。

$ sudo ip netns del ns0
$ sudo ip link del dev br0

さらに掘り下げるための資料

駆け足でしたが代表的なコンテナの要素技術を解説しました。

普段はDockerやKubernetesなどで手軽に利用しているコンテナですが、その裏ではさまざまな技術を組み合わせて作られていることがうかがえたのではないでしょうか。本稿以上にコンテナの要素技術を掘り下げたい方におすすめの資料や書籍をご紹介します。

  • コンテナ未経験新人が学ぶコンテナ技術入門

    @TokunagaKoheiさんによるコンテナ技術解説です。コンテナの概要からDocker, Kubernetesのコンテナプラットフォームの解説、本稿でも解説したコンテナの要素技術、さらにコンテナ技術の標準化とコンテナランタイム動向など、網羅的で充実した内容です。現在のコンテナ関連情報をひととおり知るにはこれ以上ない資料です。コンテナに初めて触ってから、1ヶ月でこの知識レベルとは……本当にすごいです。

  • LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術:連載|gihyo.jp … 技術評論社

    @ten_forwardさんによるコンテナ関連技術にまつわる連載で、2014年から始まり現在も更新され続けています。本稿で解説したコンテナ要素技術はもちろんのこと、それ以外の技術も深く掘り下げて解説されています。ひとつひとつのコンテナ要素技術について、より深く学ぶのにおすすめの連載です。私も大変お世話になっています。

  • Understanding and Hardening Linux Containers

    イギリスに本社があるNCC Groupのコンテナ技術の調査資料です。少々古いのですがコンテナ概要から要素技術の解説、(当時における)コンテンランタイムの比較などが掲載されています。筆者は英語が苦手ですが、コンテナを学び始めた頃は必死で読んでいました。少し前のコンテナ技術を振り返るのに良い資料です。またAbusing Privileged and Unprivileged Linux Containersでは、この記事では掘り下げなかったコンテナのセキュリティについて解説しています。こちらもおすすめです。

  • Linux Kernel Networking: Implementation and Theory

    Linuxネットワークの実装について解説した書籍です。少々古い資料ですが、NamespaceとCgroupについても触れていて、両技術をカーネルのソースコードをベースに解説しているので大変参考になります。最新のカーネルソースコードとは異なる箇所がありますが、調査のきっかけを得るには十分な内容です。

また、コンテナの要素技術の使い方を学ぶにはこの記事で登場したunshareコマンドやcapshコマンド、ip(ip netns)コマンドのソースコードを読むのがおすすめです。システムコールをどのように使っているかを知れば、他のコンテナラインタイムの実装を調査する際にも大いに役に立つでしょう。

ここから、さらに掘り下げて学びたい方はカーネルのソースコードを読みましょう。コンテナの要素技術をすべて一度に学ぶのは大変です。まずはそれぞれの概要を理解して、その中から興味をもったものに的を絞って掘り下げていくことをおすすめします。

hayajo_77@hayajo

hayajo_77
株式会社グローバルネットコアでアプリケーションエンジニアを、その後、株式会社ウォーターセルでインフラエンジニアを経て、2017年11月にはてな入社。サーバー監視サービス「Mackerel」のSREを務める。2016年ころよりコンテナ技術に関心を持ち、同技術にまつわる登壇、社内勉強会の講師活動などを行う。

【修正履歴】
ご指摘により誤字を修正しました。〔2019年7月9日〕

関連記事