コマンドを使って簡単なコンテナを作ろう

WhaTapモニタリング

2023年12月28日


blog_main

一般的に、コンテナを利用するために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が制限された部分を確認できます。

参考資料

규석님
Gyuseok Jung ([email protected])
DevOps TeamManager

今すぐWhaTapをお試しください。