今更ながら「コンテナ技術入門」をやってみた

はじめに

業務で Docker や Kubernetes などのコンテナ技術を日々利用しているが、その要素技術についてきちんと学んだことがなかったため、今更ながら「コンテナ技術入門」をやってみた

en-ambi.com

環境

M1, 2020
macOS sonoma バージョン 14.2.1

仮想サーバの用意

colima を利用して仮想サーバを立ち上げる
バージョンは 0.6.8 を利用
仮想サーバに割り当てる CPU とメモリはそれぞれ 4CPUs、8GB とする (自身のローカル環境に合わせて適宜調整すること)

$ colima start --cpu 4 --memory 8
$ colima list
PROFILE    STATUS     ARCH       CPUS    MEMORY    DISK     RUNTIME    ADDRESS
default    Running    aarch64    4       8GiB      60GiB    docker

$ colima ssh

起動した仮想サーバは Ubuntu 23.10

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 23.10"
NAME="Ubuntu"
VERSION_ID="23.10"
VERSION="23.10 (Mantic Minotaur)"
VERSION_CODENAME=mantic
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=mantic
LOGO=ubuntu-logo

ローカル環境の cgroup バージョンは v2 となっているが「コンテナ技術入門」では cgroup v1 を利用していると思われるため一部読み替えて実行していく

使用しているLinux環境のcgroupが、v1なのかv2なのかを確認する - CLOVER🍀

$ stat -fc %T /sys/fs/cgroup/
cgroup2fs

まずは必要となるコマンドをインストールする
libcap パッケージのリンクが切れていたのでリポジトリ URL はミラーリポジトリに変更する

$ sudo apt update
$ sudo apt install cgdb cgroup-tools make gcc
$ sudo git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/morgan/libcap /usr/src/libcap
$ cd /usr/src/libcap && sudo make && sudo make install

コンテナを作成する

仮想サーバの準備ができたら Docker コンテナの bash イメージを利用してファイルシステムを用意する

まずはファイルシステムとして利用するテンポラリディレクトリを作成する

$ ROOTFS=$(mktemp -d)

bash イメージのコンテナを作成し、コンテナ ID を取得する

$ CID=$(sudo docker container create bash)

コンテナのファイルシステムを tar アーカイブで出力し、作成したディレクトリに展開する

$ sudo docker container export $CID | tar -x -C $ROOTFS

/usr/local/bin/bashシンボリックリンクを作成する

$ ln -s /usr/local/bin/bash $ROOTFS/bin/bash

不要になったコンテナを削除する

$ sudo docker container rm $CID

これでこれから作成するコンテナのファイルシステムが用意できた

続いて CPU とメモリを制限するサブグループを作成しようとしたところ、uuidgen コマンドが実行できなかった

$ UUID=$(uuidgen)
bash: uuidgen: command not found

util-linux パッケージに含まれているらしいが、既にバージョン 2.39.1 をインストール済みとのこと

Ubuntu Manpage: uuidgen - create a new UUID value

$ dpkg -l | grep util-linux
ii  util-linux                     2.39.1-4ubuntu2                   arm64        miscellaneous system utilities

調べてみたところソースコードを落としてきてビルドすると util-linux に含まれるコマンド諸々が利用できるとのこと
試してみたところ問題なさそう

Linux: util-linux を gdb でデバッグする - CUBE SUGAR CONTAINER

$ cd ~
$ wget -O - https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v2.39/util-linux-2.39.1.tar.gz   | tar zxvf -
$ cd util-linux-2.39.1/
$ ./configure && make
$ ./uuidgen --version
uuidgen from util-linux 2.39.1

続いて cgcreate で実際にサブグループを作成してみる
いくつかオプションがあるが、-t-a でタスク (プロセスやスレッド) とサブグループおよび関連するファイルのオーナーを指定し、-g でサブシステム (cpu,memory など) とそれらを管理するサブグループを指定している

$ UUID=$(./uuidgen)
$ sudo cgcreate -t $(id -un):$(id -gn) -a $(id -un):$(id -gn) -g cpu,memory:$UUID

作成されたサブグループは /sys/fs/cgroup/<subgroup> で確認できる

$ ls -l /sys/fs/cgroup/$UUID
total 0
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.controllers
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.freeze
--w------- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.kill
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.max.depth
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.max.descendants
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.pressure
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.procs
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.subtree_control
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.threads
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cgroup.type
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.idle
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.max.burst
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.pressure
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.uclamp.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.uclamp.min
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.weight
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpu.weight.nice
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus.effective
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.cpus.partition
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.mems
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 cpuset.mems.effective
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.pressure
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.prio.class
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 io.weight
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.events
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.events.local
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.high
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.low
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.max
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.min
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.numa_stat
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.oom.group
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.peak
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.pressure
--w------- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.reclaim
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.stat
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.high
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.swap.peak
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.zswap.current
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 memory.zswap.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.current
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.events
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.max
-r--r--r-- 1 xxxxx xxxxx 0 Mar  1 06:56 pids.peak

次に作成したグループに CPU 30%、メモリ 10MB の制限を設けてみる
cgroup v2 では読み替えが必要で memory.limit_in_bytesmemory.maxcfs_period_uscpu.cfs_quota_uscpu.max となる

https://www.kernel.org/doc/Documentation/cgroup-v2.txt

# cgorup v1 のコマンド
$ cgset -r memory.limit_in_bytes=10000000 $UUID
# cgorup v2 のコマンド
$ cgset -r memory.max="10000000" $UUID

# cgorup v1 のコマンド
$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=300000 $UUID
# cgorup v2 のコマンド
$ cgset -r cpu.max="300000 1000000" $UUID

続いて CPU とメモリに制限を設けたサブグループにコンテナを作成しようとしたが、cgroup change of group failed エラーが発生した

$ 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'
  "
cgroup change of group failed

以下を参考にログレベルを変えてみたところ /sys/fs/cgroup/$UUID/cgroup.procs への書き込みで Permission denied が発生しているようだ

linux - Using cgroups v2 without root - Unix & Linux Stack Exchange

$ CGROUP_LOGLEVEL=DEBUG 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'
  "
Found cgroup option cpuset, count 0
Found cgroup option cpu, count 1
Found cgroup option io, count 2
Found cgroup option memory, count 3
Found cgroup option hugetlb, count 4
Found cgroup option pids, count 5
Found cgroup option rdma, count 6
Found cgroup option misc, count 7
My euid and egid is: 501,1000
Will move pid 3200 to cgroup '21f28fdd-f5bc-4453-bd03-995bb6dde1e7'
Adding controller cpu
Adding controller memory
cgroup build procs path: /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs
Warning: cannot write tid 3200 to /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs:Permission denied
Warning: cgroup_attach_task_pid failed: 50016
cgroup change of group failed

/sys/fs/cgroup/$UUID/cgroup.procs の Permission を確認してみたが問題はなさそう

$ ls -l /sys/fs/cgroup/$UUID/cgroup.procs
-rw-r--r-- 1 xxxxx xxxxx 0 Mar  1 06:59 /sys/fs/cgroup/21f28fdd-f5bc-4453-bd03-995bb6dde1e7/cgroup.procs

なにやら Linux Kernel のドキュメントによると、サブグループの cgorup.procs だけでなく、親となる cgroup.procs への書き込み権限も必要らしい

Control Group v2 — The Linux Kernel documentation

A PID can be written to migrate the process associated with the PID to the cgroup. The writer should match all of the following conditions.
- It must have write access to the "cgroup.procs" file.
- It must have write access to the "cgroup.procs" file of the common ancestor of the source and destination cgroups.

cgroup.procs への書き込み権限を付与して再度コマンドを実行してみると無事コンテナを作成できた

$ sudo chmod o+w /sys/fs/cgroup/cgroup.procs
-rw-r--rw- 1 root root 0 Mar  1 07:26 /sys/fs/cgroup/cgroup.procs

$ 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'
  "

コンテナを作成したコマンドをみていくと・・・
cgexeccgcreate で作成したサブグループ内でプロセスを実行している

cgexec -g cpu,memory:$UUID

実行しているプロセス unshare は隔離した Namespace でプロセスを実行するもので、ここでは /bin/sh -c に続くコマンドを実行している
オプション -muinpfr は Namespace での隔離対象にファイルシステムのマウントポイント、UTS (ホストネームもしくはドメイン名)、IPC (POSIX メッセージキュー、セマフォセット、共有メモリ)、ネットワーク (ルートテーブル、ファイアウォールルール、ソケットなど)、PID を含め、実行するプロセスは unshare の子プロセスとしてフォークし、現在のユーザの UID:GID を新しい Namespace では root ユーザの UID:GIDマッピングするというものらしい

unshare -muinpfr /bin/sh -c

更にコマンドを見ていくと
隔離された Namespace 上の /proc と、仮想サーバ上の $ROOTFS/proc をマウントしている

mount -t proc proc $ROOTFS/proc

仮想サーバ上のファイルシステムtty を作成し、隔離した Namespace 上の tty とマウントする

touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty)

同様に ptmx も作成してマウントする

touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx

マウントした ptmxシンボリックリンクを作成する
これでターミナルからコンテナの操作が実行できそう

ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx

続いて /dev/null を作成してマウントする

touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null

ホストネームに $UUID を指定する

/bin/hostname $UUID

capsh はケイパビリティを設定するコマンドで、--chroot オプションでプロセスのルートディレクトリを冒頭で作成したファイルシステムとし、ファイルシステムを隔離している
--drop で子プロセスの chroot ケイパビリティを剥奪している
最後に exec $CMD/bin/sh を実行している

exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'

作成できたコンテナ内でホスト名、ユーザ、プロセステーブル、マウントテーブルを確認してみる

ホスト名は UUID に書き換わっていることを確認できる

# uname -n
21f28fdd-f5bc-4453-bd03-995bb6dde1e7

ユーザは昇格したとおり root になっている

# id
uid=0(root) gid=0(root) groups=65534(nobody),0(root)

プロセスも隔離されており、プロセス /bin/sh が PID 1 となっている

# ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   13 root      0:00 ps aux

マウントした /procttyptmx/dev/null も確認できる

# mount
proc on /proc type proc (rw,relatime)
devpts on /dev/pts/0 type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
devpts on /dev/pts/ptmx type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
devtmpfs on /dev/null type devtmpfs (rw,nosuid,noexec,relatime,size=4034036k,nr_inodes=1008509,mode=755,inode64)

最後にリソースが制限されているかを確認するため yes コマンドを実行してみる

# yes >/dev/null 

別途ターミナルを立ち上げて仮想サーバに接続してからプロセスを確認してみると、unshare/bin/shyes プロセスが確認できる

$ ps f
    PID TTY      STAT   TIME COMMAND
   8339 pts/1    Ss     0:00 /bin/bash --login
   8367 pts/1    R+     0:00  \_ ps f
   2344 pts/0    Ss     0:00 /bin/bash --login
   8053 pts/0    S      0:00  \_ unshare -muinpfr …
   8054 pts/0    S      0:00      \_ /bin/sh
   8323 pts/0    R+     0:02          \_ yes

top コマンドで CPU 使用率を確認してみると 30% に制限されていることも確認できた

$ top -p 8323
top - 07:30:42 up 38 min,  2 users,  load average: 0.00, 0.00, 0.00
Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  7.3 us,  0.2 sy,  0.0 ni, 92.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
MiB Mem :   7922.4 total,   7127.7 free,    299.2 used,    656.9 buff/cache     
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   7623.2 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                             
   8323 xxxxx     20   0    1704    768    768 R  29.9   0.0   0:21.89 yes

一通り確認できたので、コンテナを終了して

# exit

サブグループとファイルシステムも削除する

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

さいごに

日頃、コンテナを起動する際にどんなことが行われているかをなんとなく理解することはできた
「コンテナ技術入門」のコンテンツはまだ続いているため、引き続きコンテナの要素技術について学び、纏めようと思う

しかし「コンテナ」とは良く言ったものだ