와탭랩스 블로그 오픈 이벤트 😃
자세히 보기
Tech
2023-10-06
コマンドを使って簡単なコンテナを作ろう

Untitled.png

一般的に、コンテナを利用するために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_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 では、特定のプロセスをネームスペースに入れると、そのネームスペースで許可するものだけを見ることができる機能である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」コマンドを使ってどのようなコマンドなのかを確認してみましょう。

 

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 -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


cgroupとは、*Control groups*を略した言葉で、特定のグループに属するプロセスが使用できるリソースを制限する機能です。その機能を介して上記で作成したコンテナがubuntu 22.04では、cgroup v2が使用されます。したがって、次のコマンドに従うと、cpuの使用量を制限できます。

 

  1. まず cgroup を使用するには、次のパッケージのインストールが必要です。

 

sudo apt-get install cgroup-tools

 

  1. まず、cpu および cpu set コントローラが /sys/fs/cgroup/cgroup.controllers ファイルで利用可能であることを確認します。

 

cat /sys/fs/cgroup/cgroup.controllers

 

該当するコマンドの実行時、以下の内容を出力する際に正常に使用可能な状況です。

 

cpuset cpu io memory hugetlb pids rdma

 

  1. CPU関連コントローラを有効にします。

 

echo "+cpu" >> /sys/fs/cgroup/cgroup.subtree_controlecho "+cpuset" >> /sys/fs/cgroup/cgroup.subtree_control

 

このコマンドは、/sys/fs/cgroup のサブグループに対して cpu、cpuset コントローラを使用できます。

  1. /sys/fs/cgroup の下に Example というサブグループを作成します。

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リソースとメモリリソースへのアクセスを継承します。

  1. CPU 関連コントローラを有効にして、CPU のみに関連するコントローラをインポートします。

 

echo "+cpu" >> /sys/fs/cgroup/Example/cgroup.subtree_controlecho "+cpuset" >> /sys/fs/cgroup/Example/cgroup.subtree_control

 

そのコマンドを介してcpuタイムを制御するコントローラのみが利用可能です。

  1. /sys/fs/cgroup/Example/tasks/ ディレクトリを作成した後、cpuコントローラを有効にします。

 

mkdir /sys/fs/cgroup/Example/tasks/echo "1" > /sys/fs/cgroup/Example/tasks/cpuset.cpus

 

そのディレクトリは、下に実際のcpuを制限するタスクを置くために使用されます。

  1. 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コアを使用します。

 

  1. コマンド実行後、unshareコマンドのpid を見つけ、次のコマンドを入力します。

 

echo> /sys/fs/cgroup/Example/tasks/cgroup.procs

 

  1. 以降topコマンドを実行すると、cpuが制限された部分を確認できます。

 

参考資料

  • 24章。cgroups-v2を使用してアプリケーションのCPU時間分配制御

  • コンテナセキュリティ - 著者:Liz Rice

 

와탭 모니터링을 무료로 체험해보세요!