一般的に、コンテナを利用するためにDockerなどのコンテナプログラムを介してコンテナを使用します。しかし、基本的なコンテナの場合、Linuxコマンドの組み合わせだけでも実現できます。今回のポストでは、Linuxコマンドを使用してコンテナがどのように分離され、どのように機能するかを説明します。
chrootによるルートディレクトリの変更
コンテナを勉強してみると、必ず耳に聞くコマンドが1つあります。chroot
です。
chroot
がどのコマンドであるかをマニュアルで確認すると、次のように記述されています。
NAME
chroot - run command or interactive shell with special root directory
SYNOPSIS
chroot - [OPTION] NEWROOT [COMMAND [ARG]...]
chroot - OPTION
DESCRIPTION
Run COMMAND with root directory set to NEWROOT.
つまり、特殊なルートディレクトリでコマンドを実行したり、インタラクティブシェルを実行させることができる命令で、chroot <ディレクトリ> <コマンド>構造で新しいルートディレクトリで命令を実行するコマンドです
chroot
がどんなコマンドなのか分かったので、実際にどのように動作するのか新しいフォルダを作成し、chroot
コマンドを実行してみる時間です。
test@jungnas:~/container$ mkdir new_root
test@jungnas:~/container$ sudo chroot new_root ls
chroot: failed to run command ‘ls’: No such file or directory
ls コマンドがないとしながら思った通り動作しません。原因は、そのフォルダが空のフォルダであるため、新しく作成されたルートで命令を実行する実行可能なファイルもないからです。このために、最も軽くてコンテナ環境でよく使われるalpine Linuxを試してみましょう。alpine Linuxの場合は、tar.gzファイルとしても提供され、この例で使用するのに適したディストリビューションです。
test@jungnas:~/container$ mkdir alpinelinux
test@jungnas:~/container$ cd alpinelinux
test@jungnas:~/container/alpinelinux$ ls
test@jungnas:~/container/alpinelinux$ wget https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.17.3-x86_64.tar.gz
これで解凍したので、alpinelinuxディレクトリでchrootコマンドを実行してみましょう。
test@jungnas:~/container/alpinelinux$ sudo chroot alpinelinux ls /
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
ls /コマンドを実行すると、解凍されたフォルダのルートディレクトリの外観を確認できます。それでは、実際にshellを実行してみてはいかがでしょうか? alpine Linuxの場合、bash shellがないのでshとして実行します。
test@jungnas:~/container/alpinelinux$ sudo chroot alpinelinux sh
/ # ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ # cat /etc/hostname
localhost
/ # cat /etc/passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
/ # whoami
root
/ # exit
test@jungnas:~/container/alpinelinux$
見ているように、実際のホストオペレーティングシステムとは別のディストリビューションのように動作することを確認できます。ただし、chrootはプロセスが親ルートを見ることができる現象があり、実際のコンテナ実装ではchrootよりもpivot_rootが好まれています。
Linux Namespace
Linux では、特定のプロセスをネームスペースに入れると、そのネームスペースで許可するものだけを見ることができる機能であるnamespace機能をサポートします。Linuxで現在動作しているネームスペイスを見たい場合は、次のコマンドで確認できます。
test@jungnas:~$ lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 2 2949039 test bash
4026531835 cgroup 2 2949039 test bash
4026531836 pid 2 2949039 test bash
4026531837 user 2 2949039 test bash
4026531838 uts 2 2949039 test bash
4026531839 ipc 2 2949039 test bash
4026531840 net 2 2949039 test bash
4026531841 mnt 2 2949039 test bash
この機能により、Linuxのさまざまな機能を分離して利用することができます。このときネームスペースを変更するには「unshare」というコマンドを利用します。「man unshare」コマンドを使ってどのようなコマンドなのかを確認してみましょう。
NAME
unshare - run program in new namespaces
SYNOPSIS
unshare [options] [program [arguments]]
DESCRIPTION
The unshare command creates new namespaces (as specified by the command-line options described below) and then executes the specified program. If program is not given, then "${SHELL}" is run (default: /bin/sh).
説明が表示されると、unshareコマンドは、以下に説明するコマンドラインオプションで指定されているように新しくネームスペースを作ります。新しくネームスペースを作ってから指定されたプログラムを実行します、という説明が確認できます。隔離対象はpidからネットワークまでほぼすべての対象ですが、この例ではpid、cgroupを隔離してコンテナにしてみます。
PID ネームスペース
コンテナ内部でpsコマンドを実行した場合は、次のようにpidが常に1であることが確認できます
/ # ps -ef
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
30 nginx 0:08 nginx: worker process
31 nginx 0:08 nginx: worker process
32 root 0:00 sh
38 root 0:00 ps -ef
これは、pidをホストから分離して個々のネームスペースを適用したためです。unshareコマンドを利用してpidに新しいネームスペースを作って実行させるコマンドは次のとおりです。
sudo unshare --pid 〈command〉
今までの説明によると、上記のコマンドを使用してshコマンドを新しいネームスペースで実行すると、すぐにpid 1でshコマンドが実行されることが見えます。実際にもそうなのか、一度試してみましょう。
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid sh
# ls
alpinelinux
# ls
sh: 2: Cannot fork
# ls
sh: 3: Cannot fork
何か変です。pid チェックをしていなかったのに変にエラーが発生します。その内容の原因はshプロセスにあります。「sudo unshare --pid sh」コマンドでは、shプロセスの親プロセスはunshareでなければなりませんが、sudoが親になっていることが原因です。これを解決するには、—forkオプションを追加するだけです。
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid --fork sh
# ps
PID TTY TIME CMD
1339 pts/1 00:00:00 sudo
1340 pts/1 00:00:00 unshare
1341 pts/1 00:00:00 sh
1342 pts/1 00:00:00 ps
# ps
PID TTY TIME CMD
1339 pts/1 00:00:00 sudo
1340 pts/1 00:00:00 unshare
1341 pts/1 00:00:00 sh
1343 pts/1 00:00:00 ps
forkの問題は解決しましたが、まだ奇妙な点が残っています。 私たちはコンテナのようにpidが1で表される部分を見たいのですが、1339のように奇妙に見えます。この現象の原因は、psコマンドを見ると分かります。
This ps works by reading the virtual files in /proc. This ps does not need to be setuid kmem or have any privileges to run. Do not give this ps any special permissions.
psコマンドは/ procディレクトリを読み込んで表現することが原因でした。私たちはchrootコマンドでルートディレクトリを変更する方法を知っています。chrootを使ってrootを変更し、/ procディレクトリを追加マウントしてpsコマンドをしてみましょう。
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid --fork chroot alpinelinux sh
/ # mount -t proc proc proc
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh
3 root 0:00 ps
しゅびよくpidが隔離されているのを見ることができます。
Cgroup
cgroupとは、*Control groups*を略した言葉で、特定のグループに属するプロセスが使用できるリソースを制限する機能です。その機能を介して上記で作成したコンテナがubuntu 22.04では、cgroup v2が使用されます。したがって、次のコマンドに従うと、cpuの使用量を制限できます。
1. まず cgroup を使用するには、次のパッケージのインストールが必要です。
sudo apt-get install cgroup-tools
2. まず、cpu および cpu set コントローラが /sys/fs/cgroup/cgroup.controllers ファイルで利用可能であることを確認します。
cat /sys/fs/cgroup/cgroup.controllers
該当するコマンドの実行時、以下の内容を出力する際に正常に使用可能な状況です。
cpuset cpu io memory hugetlb pids rdma
3. CPU関連コントローラを有効にします。
echo "+cpu" >> /sys/fs/cgroup/cgroup.subtree_control
echo "+cpuset" >> /sys/fs/cgroup/cgroup.subtree_control
このコマンドは、/sys/fs/cgroup のサブグループに対して cpu、cpuset コントローラを使用できます。
4. /sys/fs/cgroup の下に Example というサブグループを作成します。
mkdir /sys/fs/cgroup/Example/
そのフォルダを作ると、下に自動的に多くのファイルが生成されたことが確認できます。
ubuntu@ip-10-1-1-227:~/container$ ls /sys/fs/cgroup/Example/
cgroup.controllers cpu.max.burst io.prio.class memory.reclaim
cgroup.events cpu.pressure io.stat memory.stat
cgroup.freeze cpu.stat io.weight memory.swap.current
cgroup.kill cpu.uclamp.max memory.current memory.swap.events
cgroup.max.depth cpu.uclamp.min memory.events memory.swap.high
cgroup.max.descendants cpu.weight memory.events.local memory.swap.max
cgroup.pressure cpu.weight.nice memory.high memory.zswap.current
cgroup.procs cpuset.cpus memory.low memory.zswap.max
cgroup.stat cpuset.cpus.effective memory.max pids.current
cgroup.subtree_control cpuset.cpus.partition memory.min pids.events
cgroup.threads cpuset.mems memory.numa_stat pids.max
cgroup.type cpuset.mems.effective memory.oom.group pids.peak
cpu.idle io.max memory.peak
cpu.max io.pressure memory.pressure
これらのファイルは、アクティブなコントローラとしてデフォルトで新しく作成されたサブグループは、制限なしにすべてのシステムのCPUリソースとメモリリソースへのアクセスを継承します。
5. CPU 関連コントローラを有効にして、CPU のみに関連するコントローラをインポートします。
echo "+cpu" >> /sys/fs/cgroup/Example/cgroup.subtree_control
echo "+cpuset" >> /sys/fs/cgroup/Example/cgroup.subtree_control
そのコマンドを介してcpuタイムを制御するコントローラのみが利用可能です
6. /sys/fs/cgroup/Example/tasks/ ディレクトリを作成した後、cpuコントローラを有効にします。
mkdir /sys/fs/cgroup/Example/tasks/
echo "1" > /sys/fs/cgroup/Example/tasks/cpuset.cpus
そのディレクトリは、下に実際のcpuを制限するタスクを置くために使用されます。
7. CPU時間配布制御を設定して、/sys/fs/cgroup/Example/tasksサブグループ内のすべてのプロセスを1秒ごとに0.2秒間のみCPUで実行できます。つまり、1秒の5分の1になります。
echo "200000 1000000" > /sys/fs/cgroup/Example/tasks/cpu.max
8. その後、chrootをpid分離してcpuに負荷を与えることができるコマンドを実行します。
sudo unshare --pid --fork chroot alpinelinux `for i in 1; do while : ; do : ; done & done`
上記のコマンドはchrootでルートを分離し、unshare - pidでpidまで分離し、 「for i in 1; do while:; do:; done&done」命令を実行するコマンドです。上記のコマンドはcpu 1コアを使用します。
9. コマンド実行後、unshareコマンドのpid を見つけ、次のコマンドを入力します。
echo > /sys/fs/cgroup/Example/tasks/cgroup.procs
10. 以降topコマンドを実行すると、cpuが制限された部分を確認できます。
参考資料
- - 24章。cgroups-v2を使用してアプリケーションのCPU時間分配制御
- コンテナセキュリティ - 著者:Liz Rice