前回の続き
前回の記事 では通常のプロセスとコンテナプロセスを比較しながら「コンテナとは何か」について解説していきましたが、ネットワークについて解説している途中で終わってしまったので、その続きからやっていこうと思います。
なのですが、さっそく脇道にそれて nginx のコンテナイメージをカスタマイズしていこうと思います。(その方がポートバインドする nginx コンテナを起動するのに便利なので。。)
Dockerfile でコンテナイメージをカスタマイズしよう
前回の記事では nginx:latest のコンテナイメージを使用してコンテナを起動した後に、起動したコンテナ内で
① apt update
② apt install -y procps
③ /usr/share/nginx/html/hello.html の作成
を行ったと思いますが、通常はこのような運用はやりません。
なぜならコンテナは基本的にエフェメラル(一時的)なものであり、コンテナが削除されるとコンテナ内で行った変更も消えるからです。確認してみましょう。
前回作成したコンテナには procps がインストールされていて、/usr/share/nginx/html/hello.html も作成されていますね。
[root@localhost ~]# podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ac22be2830ef docker.io/library/nginx:latest nginx -g daemon o... 4 days ago Up 3 days 80/tcp festive_rhodes
[root@localhost ~]#
[root@localhost ~]# podman exec -it ac22be2830ef bash
root@ac22be2830ef:/#
root@ac22be2830ef:/# dpkg -l | grep procps #procpsパッケージの確認
ii procps 2:4.0.4-9 amd64 /proc file system utilities
root@ac22be2830ef:/#
root@ac22be2830ef:/# cat /usr/share/nginx/html/hello.html #hello.htmlの確認
This is Container Process
root@ac22be2830ef:/#
root@ac22be2830ef:/# exit
exit
[root@localhost ~]#
このコンテナを削除して、再度 nginx のコンテナを作成してみます。
[root@localhost ~]# podman rm -f ac22be2830ef
ac22be2830ef
[root@localhost ~]#
[root@localhost ~]# podman ps -a #何も表示されない(コンテナが削除された)
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@localhost ~]#
[root@localhost ~]# podman run -d docker.io/library/nginx
19af8938d3c064b76ca995631f68217a539f51179c5e7120ac29057e22c5c4f0
[root@localhost ~]#
[root@localhost ~]# podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
19af8938d3c0 docker.io/library/nginx:latest nginx -g daemon o... 5 seconds ago Up 5 seconds 80/tcp xenodochial_ride
再作成したコンテナで確認すると procps がインストールされておらず、/usr/share/nginx/html/hello.html も作成されていないことがわかります。
※ちなみにここでは試していませんが、 rm(削除)ではなく stop(停止)しただけであれば、再度 start すれば変更内容は残ったままです。
[root@localhost ~]# podman exec -it 19af8938d3c0 bash
root@19af8938d3c0:/#
root@19af8938d3c0:/# dpkg -l | grep procps
root@19af8938d3c0:/#
root@19af8938d3c0:/# cat /usr/share/nginx/html/hello.html
cat: /usr/share/nginx/html/hello.html: No such file or directory
root@19af8938d3c0:/#
root@19af8938d3c0:/# exit
exit
[root@localhost ~]#
このようにコンテナ内で行った変更はコンテナを削除すると消えてしまうため、コンテナに変更を加えたい場合はコンテナイメージを編集します。
[root@localhost ~]# mkdir image_nginx
[root@localhost ~]# cd image_nginx/
[root@localhost image_nginx]#
[root@localhost image_nginx]# cat << EOF > Dockerfile
> FROM docker.io/library/nginx:latest
>
> RUN apt update
> RUN apt install -y procps
>
> COPY hello.html /usr/share/nginx/html/
> EOF
[root@localhost image_nginx]#
[root@localhost image_nginx]# cat << EOF > hello.html
> This is Container Process
> EOF
[root@localhost image_nginx]#
[root@localhost image_nginx]# ls -l
合計 8
-rw-r--r-- 1 root root 118 8月 24 16:42 Dockerfile
-rw-r--r-- 1 root root 26 8月 24 16:43 hello.html
[root@localhost image_nginx]#
[root@localhost image_nginx]# cat Dockerfile
FROM docker.io/library/nginx:latest #nginx:latestをベースイメージに指定
RUN apt update #①実行コマンド
RUN apt install -y procps #②実行コマンド
COPY hello.html /usr/share/nginx/html/ #③ホスト上のhello.htmlをコンテナイメージの/usr/share/nginx/html/にコピー
[root@localhost image_nginx]#
[root@localhost image_nginx]# cat hello.html
This is Container Process
Dockefile と hello.html の2つのファイルを作成しました。
Dockerfile にはどのようなコンテナイメージを作るかを記載します。(記載内容の意味はコメントを確認ください。)
作成した Dockerfile もとにコンテナイメージを build します。イメージの名前もわかりやすく custom_nginx:v1 としておきます。
[root@localhost image_nginx]# podman build -t custom_nginx:v1 .
STEP 1/4: FROM docker.io/library/nginx:latest
STEP 2/4: RUN apt update
<中略>
--> 98438043a8ea
STEP 3/4: RUN apt install -y procps
<中略>
--> c537f1b46bf2
STEP 4/4: COPY hello.html /usr/share/nginx/html/
COMMIT custom_nginx:v1
--> b955b683b7af
Successfully tagged localhost/custom_nginx:v1
podman build の標準出力を見てみると、Dockerfile に書いた内容が上から実行されているのが確認できるかと思います。
イメージのビルドができたので確認してみると localhost/custom_nginx:v1 が無事に作成されていますね。(localhost/ が補完されるんですね。。)
[root@localhost image_nginx]# podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/custom_nginx v1 b955b683b7af 9 minutes ago 189 MB
docker.io/library/nginx latest f075e3f94986 4 days ago 165 MB
では、作成した custom_nginx:v1 を使ってコンテナを起動してみましょう。
[root@localhost image_nginx]# podman run -d custom_nginx:v1
fbfabf23b991281264a056d4a6d6f15d29034b349ebe3348510d58418c323149
[root@localhost image_nginx]#
[root@localhost image_nginx]# podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
19af8938d3c0 docker.io/library/nginx:latest nginx -g daemon o... 54 minutes ago Up 54 minutes 80/tcp xenodochial_ride
fbfabf23b991 localhost/custom_nginx:v1 nginx -g daemon o... 3 seconds ago Up 4 seconds 80/tcp exciting_gates
最初から procps がインストールされ、/usr/share/nginx/html/hello.html も作成された状態でコンテナが起動しています。
[root@localhost image_nginx]# podman exec -it fbfabf23b991 bash
root@fbfabf23b991:/#
root@fbfabf23b991:/# dpkg -l | grep procps
ii procps 2:4.0.4-9 amd64 /proc file system utilities
root@fbfabf23b991:/#
root@fbfabf23b991:/# cat /usr/share/nginx/html/hello.html
This is Container Process
root@fbfabf23b991:/#
root@fbfabf23b991:/# exit
exit
[root@localhost image_nginx]#
このようにコンテナに変更を加えたいときは、コンテナイメージに変更を加えてそのイメージを使ってコンテナを起動する、というのが基本的な運用になります。
では、nginx のイメージもカスタマイズできたところでネットワークの説明に戻りましょう。
ポートバインドでコンテナをあげてみよう
前回は起動したコンテナのIPを直接指定してアクセスできることを確認しました。
ただ、このコンテナのIPは起動するたびに変わります。
そのため、外部からコンテナへアクセスさせる方法として、通常はホストのポートとコンテナのポートをバインドします。確認してみましょう。
[root@localhost image_nginx]# podman run -d -p 8080:80 custom_nginx:v1
7406226064a552aadd1ab3369798f48ef7356639c96b9a7fb1859ef51205d328
[root@localhost image_nginx]#
[root@localhost image_nginx]#
[root@localhost image_nginx]# podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
19af8938d3c0 docker.io/library/nginx:latest nginx -g daemon o... About an hour ago Up About an hour 80/tcp xenodochial_ride
fbfabf23b991 localhost/custom_nginx:v1 nginx -g daemon o... 16 minutes ago Up 16 minutes 80/tcp exciting_gates
7406226064a5 localhost/custom_nginx:v1 nginx -g daemon o... 6 seconds ago Up 6 seconds 0.0.0.0:8080->80/tcp hopeful_banzai
-p オプションでホストの8080番ポートに来た通信をコンテナの80番ポートに転送するように設定したコンテナを起動しました。(一番下のコンテナ)
ホスト側でリッスンポートを確認するとちゃんと8080番ポートでリッスンしていることがわかります。
[root@localhost image_nginx]# ss -lntp | grep 8080
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("conmon",pid=114840,fd=5))
では、ホストの8080番ポートに対してアクセスしてみましょう。
[root@localhost image_nginx]# curl http://localhost:8080/hello.html
This is Container Process
hello.html にアクセスしてみると「This is Container Process」が返ってくるので、通常のプロセスの nginx ではなくコンテナプロセスの nginx のほうが応答していることがわかりますね。
ブラウザからも確認してみます。
繰り返しになりますが、ホストの80番ポートにアクセスすると、 「This is Normal Process」 のほうが返ってきます。
※ 80番ポートは既に通常プロセスの nginx が使用しているので、コンテナポートにバインドさせようとすると競合して失敗します。
[root@localhost image_nginx]# curl http://localhost/hello.html
This is Normal Process
どうやって実行環境を分離している?
前回の記事から今回の記事にかけてファイルシステム、PID、ネットワークの観点からコンテナプロセスがホストと異なる実行環境を持っていることを見てきました。
これらは linux カーネルの namespace という機能によって実現されています。namespace についてみていきましょう。
namespace とは
namespace とは 「プロセスごとに異なるシステムの見え方を作る仕組み」 です。
linux には以下の namespace があります。使用している namespace が同じなら同じものが見え、使用している namespace が異なれば当然異なるものが見えます。
プロセスがそれぞれどの namespace を使用しているかは、
/proc//ns から確認することができます。
ホストのプロセスの代表として systemd(PID 1)と通常プロセスの nginx(PID 1171)、コンテナプロセスの nginx(PID 114842)それぞれの namespace を見てみましょう。
※OSを一度再起動したので前回の記事からPIDが変わっています。
systemd と通常プロセスの nginx を比較すると mount namespace 以外すべての namespace を共有していることがわかります。
てっきりすべての namespace を共有しているものと思っていたら mount だけ共有してなかったですが、これは nginx.service の設定で PrivateTmp=yes になっており、/tmp と /var/tmp だけプロセスが認識するマウントを変えているからでした。
(通常プロセスがホストと namespace が異なっていたらちょっと話がブレるなと思いつつ。。)
[root@localhost ~]# systemctl show nginx.service | grep Private
PrivateTmp=yes ★
PrivateDevices=no
PrivateNetwork=no
PrivateUsers=no
PrivateMounts=no
PrivateIPC=no
[root@localhost ~]#
[root@localhost ~]# grep /tmp /proc/1171/mountinfo
729 711 253:0 /tmp/systemd-private-xxxxx-nginx.service-bLolVn/tmp /tmp rw,relatime shared:365 master:1 - xfs /dev/mapper/cs-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota
736 711 253:0 /var/tmp/systemd-private-xxxxx-nginx.service-T9L3Kh/tmp /var/tmp rw,relatime shared:382 master:1 - xfs /dev/mapper/cs-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota
続いてコンテナプロセスの nginx のほうを見てみると、time と user の namespace 以外すべての namespace をホストと共有していないことがわかります。
だからこれまで見てきたように、ルートファイルシステムやPID、ネットワークがホストと違っていたわけですね。
このように linux カーネルの namespace の機能がコンテナをコンテナたらしめる中核となっていることがご理解いただけたかと思います。
cgroup とは
次に、コンテナ技術のもう一つの中核となっている cgroup について説明していきます。cgroup とは 「プロセスに対してCPU・メモリなどのリソース使用量を制御する仕組み」 です。
一般的な Kubernetes の運用においては、コンテナに対して使える cpu / memory の上限(limit)を設定することが多く、それを実現しているのがこの cgroup の機能になります。実際に確認してみましょう。
プロセスは必ずどこかの cgroup に属しており、/proc//cgroup から確認することができます。
[root@localhost ~]# cat /proc/114842/cgroup
0::/machine.slice/libpod-74xxx.scope/container
nginx のコンテナプロセスは /machine.slice/libpod-74xxx.scope/container の cgroup に属していることがわかります。
現在主流となっている cgroup v2 においては cgroup は / を頂点とした階層構造になっており、
container の親 cgroup は libpod-74xxx.scope、さらにその親は machine.slice という関係になっています。
そして、親 cgroup に設定したリソース制限は子 cgroup にも継承されます。
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/memory.max
max
[root@localhost ~]#
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/libpod-74xxx.scope/memory.max
max
[root@localhost ~]#
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/libpod-74xxx.scope/container/memory.max
max
それぞれの cgroup の memory.max の値を見てみると、現状では machine.slice、libpod-74xxx.scope、container いずれの cgroup においても memory.max の値が max、つまり memory を限界まで使える設定になっていることがわかります。
では、memory を制限したコンテナを作成してみましょう。
[root@localhost ~]# podman run -d --memory=1g custom_nginx:v1
03ccb25e144d874f41ce1b9747a063c568c9032315440d7e18490ebaa0b196d3
[root@localhost ~]#
[root@localhost ~]# podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7406226064a5 localhost/custom_nginx:v1 nginx -g daemon o... 25 hours ago Up 25 hours 0.0.0.0:8080->80/tcp hopeful_banzai
03ccb25e144d localhost/custom_nginx:v1 nginx -g daemon o... 5 seconds ago Up 5 seconds 80/tcp focused_mirzakhani
[root@localhost ~]#
[root@localhost ~]# podman inspect 03ccb25e144d | jq '.[0].State.Pid'
118728
[root@localhost ~]#
[root@localhost ~]# cat /proc/118728/cgroup
0::/machine.slice/libpod-03xxx.scope/container
[root@localhost ~]#
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/memory.max
max
[root@localhost ~]#
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/libpod-03xxx.scope/memory.max
1073741824
[root@localhost ~]#
[root@localhost ~]# cat /sys/fs/cgroup/machine.slice/libpod-03xxx.scope/container/memory.max
1073741824
--memory=1g で使用できる memory 上限を 1g に設定したコンテナで確認すると、このコンテナプロセスが属する cgroup の memory.max の値が 1073741824(1024^3)になっていることが確認できます。
(正確に言うなら、/machine.slice/libpod-03xxx.scope の cgroup に制限がかかり、子である /machine.slice/libpod-03xxx.scope/container もその制限を引き継いでいる。)
このように namespace と cgroup という機能を中核にしてコンテナが作成されているわけです。
おわりに
ここまで読んでくださった方はありがとうございます。「プロセス」という観点からコンテナを見てきましたがいかがでしたでしょうか。
我ながら正直ここまで「コンテナとはなんぞや」というところを解説している記事はなかなかないんじゃないかなと自負しているので、この記事が皆様の学習の一助になったなら幸いです。


