一般的に、コンテナを利用するためにDockerなどのコンテナプログラムを介してコンテナを使用します。しかし、基本的なコンテナの場合、Linuxコマンドの組み合わせだけでも実現できます。今回のポストでは、Linuxコマンドを使用してコンテナがどのように分離され、どのように機能するかを説明します。
コンテナを勉強してみると、必ず耳に聞くコマンドが1つあります。chrootです。chrootがどのコマンドであるかをマニュアルで確認すると、次のように記述されています。
chroot - run command or interactive shell with special root directory
chroot - [OPTION] NEWROOT [COMMAND [ARG]...]
chroot - OPTION
Run COMMAND with root directory set to NEWROOT.
つまり、特殊なルートディレクトリでコマンドを実行したり、インタラクティブシェルを実行させることができる命令で、chroot <ディレクトリ> <コマンド>構造で新しいルートディレクトリで命令を実行するコマンドです。chrootがどんなコマンドなのか分かったので、実際にどのように動作するのか新しいフォルダを作成し、chrootコマンドを実行してみる時間です。
test@jungnas:~/container$ mkdir new_roottest@jungnas:~/container$ sudo chroot new_root lschroot: failed to run command ‘ls’: No such file or directory
ls コマンドがないとしながら思った通り動作しません。原因は、そのフォルダが空のフォルダであるため、新しく作成されたルートで命令を実行する実行可能なファイルもないからです。このために、最も軽くてコンテナ環境でよく使われるalpine Linuxを試してみましょう。alpine Linuxの場合は、tar.gzファイルとしても提供され、この例で使用するのに適したディストリビューションです。
test@jungnas:~/container$mkdir alpinelinuxtest@jungnas:~/container$cd alpinelinuxtest@jungnas:~/container/alpinelinux$lstest@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 /bindevetchomelibmediamntoptprocrootrunsbinsrvsystmpusrvar
ls /コマンドを実行すると、解凍されたフォルダのルートディレクトリの外観を確認できます。それでは、実際にshellを実行してみてはいかがでしょうか? alpine Linuxの場合、bash shellがないのでshとして実行します。
test@jungnas:~/container/alpinelinux$ sudo chroot alpinelinux sh/ # lsbindevetchomelibmediamntoptprocrootrunsbinsrvsystmpusrvar/ # cat /etc/hostnamelocalhost/ # cat /etc/passwdroot:x:0:0:root:/root:/bin/ashbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/mail:/sbin/nologinnews:x:9:13:news:/usr/lib/news:/sbin/nologinuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologinoperator:x:11:0:operator:/root:/sbin/nologinman:x:13:15:man:/usr/man:/sbin/nologinpostmaster:x:14:12:postmaster:/var/mail:/sbin/nologincron:x:16:16:cron:/var/spool/cron:/sbin/nologinftp:x:21:21::/var/lib/ftp:/sbin/nologinsshd:x:22:22:sshd:/dev/null:/sbin/nologinat:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologinsquid:x:31:31:Squid:/var/cache/squid:/sbin/nologinxfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologingames:x:35:35:games:/usr/games:/sbin/nologincyrus:x:85:12::/usr/cyrus:/sbin/nologinvpopmail:x:89:89::/var/vpopmail:/sbin/nologinntp:x:123:123:NTP:/var/empty:/sbin/nologinsmmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologinguest:x:405:100:guest:/dev/null:/sbin/nologinnobody:x:65534:65534:nobody:/:/sbin/nologin/ # whoamiroot/ # exittest@jungnas:~/container/alpinelinux$
見ているように、実際のホストオペレーティングシステムとは別のディストリビューションのように動作することを確認できます。ただし、chrootはプロセスが親ルートを見ることができる現象があり、実際のコンテナ実装ではchrootよりもpivot_rootが好まれています。
Linux では、特定のプロセスをネームスペースに入れると、そのネームスペースで許可するものだけを見ることができる機能であるnamespace機能をサポートします。Linuxで現在動作しているネームスペイスを見たい場合は、次のコマンドで確認できます。
test@jungnas:~$ lsnsNS TYPENPROCSPID USER COMMAND4026531834 time2 2949039 test bash4026531835 cgroup2 2949039 test bash4026531836 pid2 2949039 test bash4026531837 user2 2949039 test bash4026531838 uts2 2949039 test bash4026531839 ipc2 2949039 test bash4026531840 net2 2949039 test bash4026531841 mnt2 2949039 test bash
この機能により、Linuxのさまざまな機能を分離して利用することができます。このときネームスペースを変更するには「unshare」というコマンドを利用します。「man unshare」コマンドを使ってどのようなコマンドなのかを確認してみましょう。
unshare - run program in new namespaces
unshare [options] [program [arguments]]
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を隔離してコンテナにしてみます。
コンテナ内部でpsコマンドを実行した場合は、次のようにpidが常に1であることが確認できます。
/ # ps -efPIDUSERTIMECOMMAND1 root0:00 nginx: master process nginx -g daemon off;30 nginx0:08 nginx: worker process31 nginx0:08 nginx: worker process32 root0:00 sh38 root0: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# lsalpinelinux# lssh: 2: Cannot fork# lssh: 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# psPID TTYTIME CMD1339 pts/100:00:00 sudo1340 pts/100:00:00 unshare1341 pts/100:00:00 sh1342 pts/100:00:00 ps# psPID TTYTIME CMD1339 pts/100:00:00 sudo1340 pts/100:00:00 unshare1341 pts/100:00:00 sh1343 pts/100: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/ # psPIDUSERTIMECOMMAND1 root0:00 sh3 root0:00 ps
しゅびよくpidが隔離されているのを見ることができます。
cgroupとは、*Control groups*を略した言葉で、特定のグループに属するプロセスが使用できるリソースを制限する機能です。その機能を介して上記で作成したコンテナがubuntu 22.04では、cgroup v2が使用されます。したがって、次のコマンドに従うと、cpuの使用量を制限できます。
sudo apt-get install cgroup-tools
cat /sys/fs/cgroup/cgroup.controllers
該当するコマンドの実行時、以下の内容を出力する際に正常に使用可能な状況です。
cpuset cpu io memory hugetlb pids rdma
echo "+cpu" >> /sys/fs/cgroup/cgroup.subtree_controlecho "+cpuset" >> /sys/fs/cgroup/cgroup.subtree_control
このコマンドは、/sys/fs/cgroup のサブグループに対して cpu、cpuset コントローラを使用できます。
mkdir /sys/fs/cgroup/Example/
そのフォルダを作ると、下に自動的に多くのファイルが生成されたことが確認できます。
ubuntu@ip-10-1-1-227:~/container$ ls /sys/fs/cgroup/Example/cgroup.controllerscpu.max.burstio.prio.classmemory.reclaimcgroup.eventscpu.pressureio.statmemory.statcgroup.freezecpu.statio.weightmemory.swap.currentcgroup.killcpu.uclamp.maxmemory.currentmemory.swap.eventscgroup.max.depthcpu.uclamp.minmemory.eventsmemory.swap.highcgroup.max.descendantscpu.weightmemory.events.localmemory.swap.maxcgroup.pressurecpu.weight.nicememory.highmemory.zswap.currentcgroup.procscpuset.cpusmemory.lowmemory.zswap.maxcgroup.statcpuset.cpus.effectivememory.maxpids.currentcgroup.subtree_controlcpuset.cpus.partitionmemory.minpids.eventscgroup.threadscpuset.memsmemory.numa_statpids.maxcgroup.typecpuset.mems.effectivememory.oom.grouppids.peakcpu.idleio.maxmemory.peakcpu.maxio.pressurememory.pressure
これらのファイルは、アクティブなコントローラとしてデフォルトで新しく作成されたサブグループは、制限なしにすべてのシステムのCPUリソースとメモリリソースへのアクセスを継承します。
echo "+cpu" >> /sys/fs/cgroup/Example/cgroup.subtree_controlecho "+cpuset" >> /sys/fs/cgroup/Example/cgroup.subtree_control
そのコマンドを介してcpuタイムを制御するコントローラのみが利用可能です。
mkdir /sys/fs/cgroup/Example/tasks/echo "1" > /sys/fs/cgroup/Example/tasks/cpuset.cpus
そのディレクトリは、下に実際のcpuを制限するタスクを置くために使用されます。
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コアを使用します。
echo> /sys/fs/cgroup/Example/tasks/cgroup.procs