前回 podman での複数コンテナの連携のために pod の扱いについて調べていたら、 podman-pod-create コマンドの --share
オプションによる「名前空間が共有される」とは具体的にどういう現象なのかよくわからなかったので、簡単なハンズオンで直感的に理解できるようにしてみました。
はじめに
この記事は読み終わる頃に以下のことが分かるようになるよう目指して書きました。
- コンテナ技術の実現に利用されている、 Linux の名前空間とは何か。
- 名前空間が共有 / 分離されるとは具体的にはどういう状況か。
- Podman によるポッド作成時に共有 / 分離が指定可能な ipc, net, pid, user, uts の 5 種類の名前空間は何を管理しているのか。
--share namespace とは
podman pod create コマンドでポッドを作成する際に使えるオプションの一つです。
役割と書式については、libpod リポジトリの説明によるとこうです。
####--share=namespace
A comma delimited list of kernel namespaces to share. If none or "" is specified, no namespaces will be shared. The namespaces to choose from are ipc, net, pid, user, uts.
- ポッド内で共有する名前空間を指定するためのオプション。
- コンマ区切りのリストで複数指定。
- none か、空集合が指定された場合何も共有されない。
- ipc, net, ipd, user, uts の 5 つの名前空間から指定可能。
あー、なるほど。**完全に理解しました。**過不足ない説明ですが、理解するにはいくらかの前提知識が必要そうです。
そもそも名前空間とは
おばあちゃんが言っていた。何かを調べるときは必ず一次ソースから見ろと。
namespaces の man ページ によると以下の通りです。
名前空間は、 グローバルシステムリソースを抽象化層で覆うことで、 名前空間内のプロセスに対して、 自分たちが専用の分離されたグローバルリソースを持っているかのように見せる仕組みである。 グローバルリソースへの変更は、 名前空間のメンバーである他のプロセスには見えるが、 それ以外のプロセスには見えない。 名前空間の一つの利用方法はコンテナーの実装である。
ふ、ふーん。そういうことだったんですね。いや知ってましたよ。本当ですよ!なら PID 名前空間を例に説明しますね!?
- ホスト上のリソースは抽象化レイヤーで覆い隠されるため、コンテナ内のプロセスはホスト上で動作しているプロセスの PID が見えません。
- コンテナのプロセスには、自分たちがホストから隔離されているという認識はなく、むしろシステム全体を占有していると思っています。
- ホスト上に systemd が PID 1 で動作しているにも関わらず、コンテナ上で PID 1 で別のプロセスが動作できるのはプロセスが異なる PID 名前空間に属しているからです。
つまり、ある学校の同じクラス内にゆうた君が 二人いたら、呼ばれた時にどちらを指すか一意に定まらない ので困りますが、 二人のゆうた君が A 組と B 組に分かれて存在している分には衝突しない ということです。
名前空間の利用例
この仕組みを使って、プロセスだけではなく様々なリソースについてホストから切り離された空間を確保していくと……なんかハードウェアのエミュレーションをしているわけでもないのにホスト上で動作する 仮想マシンみたいなものが作れそう な予感がしませんか?別個のマシンかのように振る舞うのですから、例えば以下のような特長を持っていてもおかしくはないはず……というかそうではないとおかしいですよね。名前空間を分離によって、これらは実現できるはずです。
- 独自の NIC, IP, ファイアウォール設定、ルーティングテーブル を持ち、ホストマシンをネットワーク上の他のマシンとして認識する。(ホストからの network namespace の分離。)
- 独自のユーザ、ホストネームを持つ。(ホストからの user namespace, uts namespace の分離。)
- 独自にプロセス同士が通信するための共有メモリを持つ。(ホストからの ipc namespace の分離。)
さて、もうお気づきかと思いますが 様々なリソースについてホストから切り離された空間内でプロセスを実行すること こそコンテナ技術の正体です。しかし、「ホストから切り離すのはともかく、なんでもかんでもコンテナ間でキッチリ分離していると複数のプロセスで連携したいときに不便だなあ」ということで、複数のコンテナ間で一部の名前空間を共有して便利に扱えるようにしたのがポッドです。
次の章からは、ある名前空間が共有されている時とそうでない時の違いについて、Podman でポッドを作成してポッド内のコンテナの振る舞いの違いを比較することで見ていきます。
network namespace
network namespace は NIC やルーティングテーブル、ファイアウォールのポリシー設定などのネットワークに関する情報を分離させるための仕組みです。
最初に OS 上に存在している network namespace を以下のコマンドで確認します。
何も出力されません。まだ network namespace が作られていないことを意味します。
# ip netns
--share net ポッドの場合
podman で network namespace を共有するポッド netshared を作成します。ホスト側の 8080 番から ポッドの 80 番へ転送するポートフォワーディングも設定します。
ポッド内では net1 という名前の Apache コンテナが動作しています。
# podman pod create --name netshared --share net -p 8080:80
# podman run --pod netshared --rm -d --name net1 httpd
インフラコンテナと httpd コンテナが作られていることが確認できます。
インフラコンテナはポッドと関連付けられている名前空間を保持するための、ポッド管理用コンテナです。常にスリープ状態にあり、基本的に何もしません。
# podman container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d320abc7fa5 docker.io/library/httpd:latest httpd-foreground 41 seconds ago Up 40 seconds ago 0.0.0.0:8080->80/tcp net1
76630b31f5c6 k8s.gcr.io/pause:3.1 51 seconds ago Up 41 seconds ago 0.0.0.0:8080->80/tcp d1d99b6ed720-infra
インフラコンテナと net1 コンテナの IP アドレスを調べてみます。
インフラコンテナには IP アドレス 10.88.0.83/16
が割り振られていますが、net1 コンテナにはありません。
# podman inspect d1d99b6ed720-infra | grep -E "^.*\"IPAddress" -A2
"IPAddress": "10.88.0.83",
"IPPrefixLen": 16,
"IPv6Gateway": "",
# podman inspect net1 | grep -E "^.*\"IPAddress" -A1
"IPAddress": "",
"IPPrefixLen": 0,
ポッドを作成したところで、今一度 network namespace を確認してみます。
新しい network namespace が 1 つ作成されていることがわかります。
# ip netns
cni-07de2651-2761-c638-a1fd-2af594ad9503 (id: 0)
この network namespace は、先ほど作成したポッド netshared が利用しているものです。
この network namespace 上で ip a
コマンドを実行すると、netshared の仮想 NIC の情報が返ってきます。
# ip netns exec cni-07de2651-2761-c638-a1fd-2af594ad9503 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0@if85: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:71:25:ce:de:49 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.88.0.83/16 brd 10.88.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::71:25ff:fece:de49/64 scope link
valid_lft forever preferred_lft forever
eth0@if85
が 10.88.0.83/16
の IP アドレスを持っていますね。先ほど確認したインフラコンテナの IP と一致します。
ポートフォワーディングも機能するか確認してみます。
# curl localhost:8080
<html><body><h1>It works!</h1></body></html>
Apache のテストページが返ってきました。 Apache コンテナが IP アドレスを持っていなかったにも関わらず応答が返ってくるのは、Apache コンテナがインフラコンテナの仮想 NIC を共有しているためです。
--share none ポッドの場合
同じ要領で今度は 名前空間を何も共有しないポッドを作成します。ポートフォワーディングが 9090->80
になったことと、--share none
になったこと以外特に変わったことはありません。
# podman pod create --name none --share none -p 9090:80
# podman run --pod none --rm -d --name none1 -d httpd
# podman start 55db2b908174-infra
# podman container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8b91672858a9 docker.io/library/httpd:latest httpd-foreground 4 minutes ago Up 4 minutes ago none1
f4069bf6aad1 k8s.gcr.io/pause:3.1 4 minutes ago Up 3 minutes ago 0.0.0.0:9090->80/tcp 55db2b908174-infra
6d320abc7fa5 docker.io/library/httpd:latest httpd-foreground 10 minutes ago Up 40 seconds ago 0.0.0.0:8080->80/tcp net1
76630b31f5c6 k8s.gcr.io/pause:3.1 10 minutes ago Up 41 seconds ago 0.0.0.0:8080->80/tcp d1d99b6ed720-infra
PORTS 列に着目すると、 netshared ポッドの時とは違い Apache コンテナに ポートフォワーディングの設定がありません。ポッド作成時に指定した -p オプションによるポートフォワーディングの設定は、実際にはインフラコンテナへのフォワーディング設定のようです。加えて、インフラコンテナと Apache コンテナの仮想 NIC が共有されていないため Apache コンテナにはポートフォワーディングされないようです。共有されていない時、Apache コンテナの仮想 NIC ってどうなっているんでしょう。
none ポッドに属するコンテナの IP を調べてみます。
# podman inspect 55db2b908174-infra | grep -E "^.*\"IPAddress" -A1
"IPAddress": "10.88.0.85",
"IPPrefixLen": 16,
# podman inspect none1 | grep -E "^.*\"IPAddress" -A1
"IPAddress": "10.88.0.84",
"IPPrefixLen": 16,
netshared ポッドではインフラコンテナのみが IP アドレスを持っていましたが、今度はどちらのコンテナも IP アドレスを持っています。Apache コンテナは独自の仮想 NIC を持っているようです。もしかして、ポッド内で network namespace が共有されない場合、コンテナが作られるたびに独自のネットワーク名前空間が増え続けるのでは?
ip netns
で確認してみましょう。
# ip netns
cni-209074d8-5a08-7f59-bb73-6b258c228cf4 (id: 2)
cni-dc45758c-a9e2-91a2-db2c-a0f3406b9568 (id: 1)
cni-07de2651-2761-c638-a1fd-2af594ad9503 (id: 0)
先ほどの netshared ポッド用の network namespace に加えて 2 つの network namespace が作成されています。
id: 1 の network namespace には末尾 84 のアドレスを持つ仮想 NIC が確認できます。これは none ポッドのインフラコンテナのものです。
# ip netns exec cni-dc45758c-a9e2-91a2-db2c-a0f3406b9568 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0@if86: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 7a:11:aa:8e:52:ef brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.88.0.84/16 brd 10.88.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::7811:aaff:fe8e:52ef/64 scope link
valid_lft forever preferred_lft forever
id: 2 の network namespace には末尾 85 のアドレスを持つ仮想 NIC が確認できます。これは Apache を動作させている none1 コンテナのものです。
# ip netns exec cni-209074d8-5a08-7f59-bb73-6b258c228cf4 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0@if87: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether b2:93:94:3c:05:7e brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.88.0.85/16 brd 10.88.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::b093:94ff:fe3c:57e/64 scope link
valid_lft forever preferred_lft forever
ポートフォワーディングの動作も確認します。
netshared ポッドと違い、localhost:9090 への curl で Apache から応答が返りません。スリープ状態にあるインフラコンテナへの curl となるからです。
そういう点で言えば確かに netshared ポッドで行った localhost:8080 への curl も同じインフラコンテナへの curl と言えますが、netshared ポッドはインフラコンテナと Apache コンテナが仮想 NIC を共有していたので Apache コンテナへリクエストが届いていました。まるで複数のコンテナが繋がって、 1 つの仮想マシンとして動作しているかのようですね。
なお、Apache が動作している none1(10.88.0.84) に直接 curl すると応答が返るので Apache 自体は動作しているようです。
# curl localhost:9090
curl: (7) Failed to connect to localhost port 9090: 接続を拒否されました
# curl 10.88.0.84
<html><body><h1>It works!</h1></body></html>
# curl 10.88.0.85
curl: (7) Failed to connect to 10.88.0.85 port 80: 接続を拒否されました
念のため確認しますが、 firewalld によって弾かれたものではなさそうです。
# firewall-cmd --list-all --zone=public | grep -E "^\s*ports"
ports: 8080/tcp 9090/tcp
コンテナ間で network namespace が共有されない場合の挙動について確認できました。
uts namespace
ホスト名やNISドメイン名を分離する名前空間です。
UTS は Unix Time-sharing System(UNIX で採用されていた、一台のコンピュータを複数のユーザで扱うための仕組み)の略ですが、今はその意味は失われているようです。時間も特に関係ありません。
NIS は複数のコンピュータを一元管理するために用いられる仕組みで、NIS ドメインはその仕組み用の識別子です。
今回は hostname の分離について着目していきます。
--share uts ポッドの場合
uts namespace を共有するポッド utsshared を作成し、中に Apache コンテナ uts1 と CentOS7 コンテナ uts2 を作成します。
# podman pod create --name utsshared --share uts
# podman run -d --pod utsshared --name uts1 --rm httpd
# podman run -it -d --pod utsshared --name uts2 --rm centos:7
uts1 と uts2 のホスト名を表示します。ポッド名と同じ、utsshared が返りました。確かに uts namespace が共有されているようです。
# podman exec uts1 hostname
utsshared
# podman exec uts2 hostname
utsshared
--share none ポッドの場合
先ほど作成した none ポッドで同様の環境を作ってホスト名を表示させてみます。
# podman exec none1 hostname
8b91672858a9
# podman exec none2 hostname
692b66d45c4a
none1 と none2 のホスト名は自身のコンテナ ID と同じ内容でした。uts namespace は共有されていないと言えます。
ipc namespace
プロセス間通信 (Inter-Process Communication) の名前空間です。
ipc namespace が共有されているとプロセス間での通信が行えます。同じポッドに属さないコンテナ同士、およびコンテナ-ホスト間では通常分離されますが、同一ポッド内に属するコンテナ同士は ipc namespace を共有するためプロセス間通信が行えます。
同一ポッド内の IPC 通信のデモを行うためこちらのイメージをお借りしました。
https://github.com/allingeek/ch6_ipc/tree/e9fa9a13198903bebcd983bf88bcb75950823d85
FROM ubuntu:latest
RUN apt-get update && apt-get -y install gcc libc-dev
COPY . /work/ipc
WORKDIR /work/ipc
RUN gcc -o ipc ipc.c -lrt
ENTRYPOINT ["./ipc"]
中に ipc
という実行ファイルが入った Ubuntu イメージです。
ipc
の実行時に -producer
を引数にすると ランダムな数字メッセージと終了メッセージが送信されます。
ipc
の実行時に -consumer
を引数にすると -producer
が出力したメッセージを受信し、標準出力へ表示します。
--share ipc ポッドの場合
例によって、 ipc namespace を共有したポッド ipcshared を作成し、中に -producer
となる ipc1 と、-consumer
となる ipc2 を作成します。
# podman pod create --name ipcshared --share ipc
# podman run -d --pod ipcshared --name ipc1 --entrypoint /work/ipc/ipc ch6_ipc -producer
# podman run -d --pod ipcshared --name ipc2 --entrypoint /work/ipc/ipc ch6_ipc -consumer
送受信が行えているか、ログファイルから確認します。
# podman logs --tail 5 ipc1 | tac
Produced: 86
Produced: 86
Produced: 67
Produced: 2b
Produced: b9
# podman logs --tail 6 ipc2 | tac
Consumed: 86
Consumed: 86
Consumed: 67
Consumed: 2b
Consumed: b9
Consumed: done
producer と consumer が出力している数字が一致しています。送受信が正しく行えていることを確認できました。
--share none ポッドの場合
ipc namespace を共有していない none ポッドでも同様の手順を実行します。こちらは consumer がメッセージの受け取りに失敗しています。
# podman pod create --name none --share none
# podman run -d --pod ipcshared --name ipc1 --entrypoint /work/ipc/ipc ch6_ipc -producer
# podman run -d --pod ipcshared --name ipc2 --entrypoint /work/ipc/ipc ch6_ipc -consumer
# podman logs --tail 5 ipc_unshare1
Produced: 9c
Produced: 98
Produced: 53
Produced: b9
Produced: 45
# podman logs --tail 6 ipc_unshare2
Either the producer has not been started or maybe I cannot access the same memory...
ipc namespace の分離によってポッド内のプロセスが通信できなくなりました。
user namespace
UID や GID を分離させる名前空間です。
ドキュメントを見る感じこれもいじれそうだったのですが、未実装のようです。
# podman pod create --name usershared --share user
Error: unable to create pod: User sharing functionality not supported on pod level
# podman pod create --share help
Error: unable to create pod: Invalid kernel namespace to share: help. Options are: net, pid, ipc, uts or none
pid namespace
冒頭でも触れた、プロセス番号に関する名前空間です。
--share pid ポッドの場合
pid namespace が共有されたポッド pidshared を作成します。
Apache コンテナを pid1, MySQL コンテナを pid2, CentOS7 コンテナを pid3 とし、 pid3 の中に入ります。
# podman pod create --share pid --name pidshared
# podman run -d --pod pidshared --name pid1 --rm httpd
# podman run -d --pod pidshared --name pid2 --rm -e MYSQL_ROOT_PASSWORD=password mysql
# podman run -it --pod pidshared --name pid3 --rm centos:7 /bin/bash
[container_id /]#
プロセスを確認します。
[container_id /]# ps -ef
root 1 0 0 01:33 ? 00:00:00 /pause
root 114 0 0 01:44 ? 00:00:00 httpd -DFOREGROUND
bin 120 114 0 01:44 ? 00:00:00 httpd -DFOREGROUND
bin 121 114 0 01:44 ? 00:00:00 httpd -DFOREGROUND
bin 122 114 0 01:44 ? 00:00:00 httpd -DFOREGROUND
999 284 0 1 01:48 ? 00:00:02 mysqld
root 484 0 4 01:51 pts/0 00:00:00 /bin/bash
root 499 484 0 01:51 pts/0 00:00:00 ps -ef
他のコンテナで動作している httpd と mysqld のプロセスが見えます。
--share none ポッドの場合
none ポッドで同様の手順を実行します。
# podman pod create --name none
# podman run -d --pod none --name none1 --rm httpd
# podman run -d --pod none --name none2 --rm -e MYSQL_ROOT_PASSWORD=password mysql
# podman run -it --pod none --name none3 --rm centos:7 /bin/bash
[container_id /]#
[container_id /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 12 02:02 pts/0 00:00:00 /bin/bash
root 14 1 0 02:02 pts/0 00:00:00 ps -ef
pid3 コンテナ内で動作しているプロセスしか見えません。pid namespace が共有されていないためですね。
さいごに
Podman によって作成されたポッドで、デフォルトで共有されている名前空間は net,uts,ipc の 3 つです。
よって、デフォルトのポッドは以下の特徴を持ちます。
- ポッド内のコンテナはインフラコンテナと仮想 NIC を共有する。(network namespace の共有)
- ポッド名と同じホスト名を持つ。(uts namespace の共有)
- 同一ポッド上に存在するプロセス同士でプロセス間通信が可能。(ipc namespace の共有)
- ポッド外のコンテナおよびホスト上に存在するユーザは見えない。(user namespace の分離)
- ポッド外のコンテナおよびホスト上に存在するプロセスは見えない。(pid namespace の分離)
つまり、ポッド内のコンテナたちは、ポッドに属さないバラバラのコンテナ群とは違い ネットワーク的には同じマシン のように見え、プロセス間通信 (IPC) によるやり取りが可能 であるということです。1 つのコンテナでは 1 つのプロセスを動作させるべきという原則によって生じる不便を、一部の名前空間をコンテナ間で共有することで克服しているわけですね。1 コンテナ 1 プロセスの原則については、PID 1 問題や、コンテナを使い捨てる前提で運用した時に都合がいいとか色々理由はあるのですが、それを書くには余白が狭すぎるのでこのあたりで。
参考
IPC 通信のデモの際参考にさせていただきました。
Kubernetesによる同一Pod内コンテナのプロセス間通信
https://qiita.com/mamorita/items/15437a1dbcc00919fa4e