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

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

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

こんにちは。株式会社はてなでサーバー監視サービス「Mackerel」のSREを務めるhayajo_771@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を確認する

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く