Podman の CLI はほぼ Docker 互換だから、コマンドに戸惑わず同じ使用感で使えるよって言うけど、 ネットワークの仕組みは違うみたいだし、公式サポートの compose も無い Podman でコンテナ同士の疎通と連携はどうすればいいの!?と迷える子羊化していた自分にこの記事を送ります。
はじめに
この記事では Podman-Compose の使い方については詳しく触れていません。サードパーティ製のツールである Podman-Compose に代わって、 Podman の標準機能である podman generate kube
と podman play kube
を使って起動、実行、停止までを完結してみようと試みた内容です。
背景
CentOS8, RHEL8 で最も衝撃的だった変更の一つは何か?
と聞かれた時、 Docker のサポート廃止と Podman の採用を挙げる方は結構多いのではないでしょうか。
そしてこれを挙げる人たちはこうも思っているはずです。
ああ、 Podman も使えるようにしとかなきゃ……
RHEL 系 OS は商用利用可能な Linux として多くの信頼を得ていますし、 4 年後には CentOS7 のサポート終了も迫る状況でもあります。「じゃあ Ubuntu で Docker 使えばいいや」とはいかない人、そう例えば自分のためにこの記事を書きます。
幸いなことに、 Podman のコマンド群はほぼ Docker 互換であるためコンテナを操作する分には簡単に移行できますが、一つ問題があります。
複数のコンテナを連携させるにはどうすれば?
Compose は滅びぬ!サードパーティで蘇るさ!
libpod (Podman のコンテナ/ポッド作成を直接的に担っているライブラリ) の github リポジトリの Out of scope の章にはこんな記述があります。
Out of scope
Supporting docker-compose. We believe that Kubernetes is the defacto standard for composing Pods and for orchestrating containers, making Kubernetes YAML a defacto standard file format. Hence, Podman allows the creation and execution of Pods from a Kubernetes YAML file (see podman-play-kube). Podman can also generate Kubernetes YAML based on a container or Pod (see podman-generate-kube), which allows for an easy transition from a local development environment to a production Kubernetes cluster. If Kubernetes does not fit your requirements, there are other third-party tools that support the docker-compose format such as kompose and podman-compose that might be appropriate for your environment. This situation may change with the addition of the REST API.
重要なところを要約すると……
- docker-compose はサポートしません。
- 我々は Kubernetes がコンテナオーケストレータのデファクトスタンダードであると考えています。
- よって、Podman にも コンテナ/ポッドに基づいた Kubernetes YAML ファイルの生成と、逆に Kubernetes YAML ファイルに基づいて コンテナ/ポッドを実行することを可能としました。
といったところでしょうか。 Kubernetes YAML によるコンテナオーケストレーションに前向きな考えを示しており、docker-compose が担っていたポジションもこれに統合したいようです。
サードパーティ製の podman-compose は存在しています。
先日 Keycloak コンテナを構築した際には docker-compose 用の資源を流用できるツールとして大いに有用でしたが、コンテナ作成時に一部のパラメータ指定に未対応であったりと、込み入ったことをする際に困る場合があるようで、成熟にはまだ時間がかかりそうです。
流行りの Kubernetes YAML はお嫌いですか?
……というわけで RHEL 系 OS でコンテナをやろうと思ったらもうポッドという概念を避けては通れないようです。
ネットワークの代わりにポッドを作成し、 YAML ファイルを 1 から書くとまでいかなくてもファイルの内容の妥当性を吟味できる程度には Kubernetes YAML ファイルへの理解を深めましょう。
大丈夫です。不安になるのはそう、ただ以下が分からないから、それだけです。
- ポッドの作成時にどんなパラメータを与えれば?コンテナをポッドに所属させるには?
- Kubernetes YAML の書式は docker-compose とどう違う?
podman-compose が実際に行っている作業は、 Podman CLI を利用したポッドの構築です。
つまり podman-compose は Podman コマンドを我々に代わって実行しているだけです。
podman-compose up
で実行されているコマンド群の中に podman pod create
コマンドがあったはずですね。
docker-compose 互換を目指すこのツールが実行するものであればそれがヒントになるのでは?
今一度、 podman-compose -f keycloak-postgres.yml up -d
でポッドを実行してみましょう。
そしてそれを基に Kubernetes YAML も生成してみましょう。知りたいことはきっとこの中にあるはずです。
1. keycloak-postgres ポッドを構築して実行コマンドを見る。
ついさっき触れましたが、 podman-compose up
コマンドを実行した時流れるのは、実行されている Podman コマンド群とその結果です。
ということは、podman-compose up
コマンドの標準出力をファイルに収めて検索かけたら、 podman create
コマンドにどんなパラメータを渡しているのか確かめられるはずですね。
# podman-compose -f keycloak-postgres.yml up -d | tee pcup_stdout
# grep "podman pod create" pcup_stdout
podman pod create --name=docker-compose-examples --share net -p 8080:8080 -p 8443:8443
ありました。あとは コマンドリファレンス 引いて 各オプションの意味を調べるだけです。簡単ですね。podman pod create についてはこのページです。
podman pod create
ポッドを新規作成するためのコマンド。実行時、ポッド ID が標準出力に出力されます。
ポッド作成後コンテナをポッドに追加するには podman create --pod <pod_id | pod_name>
を実行すると良いそう。
また、ポッドの作成時にはインフラコンテナが作成ポッド内に作成されます。
インフラコンテナはポッドに関連付けられている名前空間の保持を行うことや、ポッドへコンテナを追加する際にその紐づけ役となる管理用コンテナです。作成後、常にスリープ状態にあり特に何もしません。
--name=docker-compose-examples
ポッドへ名前を割り当てるオプション。分かりやすい名前を付けておくと後々コマンドで指定するときとかも楽なので基本やっておくとよいと思います。
--share net
共有する名前空間を指定するオプション。指定可能なのは ipc, net, pid, user, uts
の 5 つ。カンマ区切りのリストで複数指定が可能。
この例だと ネットワーク名前空間 (netns) をポッド内で共有する、という意味になります。
具体的にどういう状況なのか要点だけ言うと……
- このポッドに属するコンテナ群は個別に仮想 NIC を持たず、インフラコンテナの仮想 NIC を共有する。
- 何らかの IP ベースの通信の宛先に localhost を指定するとコンテナ同士が疎通が取れる。(コンテナ同士が独立した netns を持っていると localhost で他のコンテナには届かない。)
- ポッドは、ホストや他のコンテナ/ポッドとは独立したルーティングテーブルやファイアウォールなどのネットワーク情報を持つ。
ネットワーク名前空間についてこの場で多くは語りませんが、コンテナやポッドを作った後に ip netns
, ip netns exec cni-hogehoge ip a
, ip netns exec cni-hogehoge firewall-cmd --list-all-zones
とかやってみると直感的に理解しやすいのではないかと思います。
ちなみに --share
オプションを使用しなかった場合、 ipc, net, uts
の3つが共有されます。
# podman pod create -n defaultpod
# podman pod inspect defaultpod | grep share
"sharesCgroup": true,
"sharesIpc": true,
"sharesNet": true,
"sharesUts": true,
--share net
だと ipc, uts が共有されなくなります。コマンドラインで指定した値でデフォルト値を上書きする仕様なんですね。
# podman pod inspect docker-compose-examples | grep share
"sharesCgroup": true,
"sharesNet": true,
-p 8080:8080 -p 8443:8443
--published
です。ポートフォワーディングの指定です。この例だとホストの 8080,8443 に接続された時にポッド上の同番号ポートに転送してます。
実際にはホストからインフラコンテナへのポートフォワーディングを設定しています。
つまり、これは上記の --share net
が有効な時に意味のある設定です。
ホストからインフラコンテナへポートフォワーディングしても、インフラコンテナと他のコンテナが NIC を共有していなければ、ただずっとスリープ状態にあるコンテナに通信が飛ぶだけで終わってしまいます。
試しに netns を共有してないポッドで -p オプションを使用し、コンテナを作ってポートフォワーディング設定を確認してみましょう。
# podman pod create --share pid -n net_unshared -p 5555:5555
# podman run -it -d --pod pidpublish --name nu1 centos:7 /bin/bash
# podman run -it -d --pod pidpublish --name nu2 centos:7 /bin/bash
# podman container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba5b724324f3 docker.io/library/centos:7 /bin/bash About an hour ago Up About an hour ago nu2
66fc35c5fe0b docker.io/library/centos:7 /bin/bash About an hour ago Up About an hour ago nu1
32d407c9b16f k8s.gcr.io/pause:3.1 About an hour ago Up About an hour ago 0.0.0.0:5555->5555/tcp bf0bbaf4280e-infra
一番下にあるインフラコンテナ (bf0bbaf4280e-infra) の PORTS 列には 0.0.0.0:5555->5555/tcp がある一方で、コンテナ nu1 と nu2 の PORTS 列には 同様の記述がありません。どうやら PORTS 列はコンテナ内の netns を参照してここへ表示しているようですね。
1 章のまとめ
- 後の利便性のためポッドに名前は付ける。
- ネットワーク名前空間をポッド内で共有 (
--share net
相当の設定。デフォルトのままでも可。) した上で-p host_port:pod_port
でポートフォワーディングを設定する。 - ネットワーク名前空間を共有すると、 1 つの仮想 NIC に複数のコンテナがぶら下がっているような状態になる。
2. Kubernetes YAML にコンバートして docker-compose.yml と比較する。
docker-compose 用の yml ファイルからポッドを構築して、それを podman generate kube
で kubernetes YAML ファイルへ変換します。
つまり、これを
version: '3'
volumes:
postgres_data:
driver: local
services:
postgres:
image: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:latest
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pa55w0rd
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it.
#JDBC_PARAMS: "ssl=true"
ports:
- 8080:8080
depends_on:
- postgres
こうして
# podman-compose -f keycloak-postgres.yml up -d
こうじゃ!
# podman generate kube docker-compose-examples > kubernetes-converted.yml
できました。
# Generation of Kubernetes YAML is still under development!
#
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-1.6.4
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2020-07-03T02:38:36Z"
labels:
app: docker-compose-examples
name: docker-compose-examples
spec:
containers:
- command:
- -b
- 0.0.0.0
env:
- name: PATH
value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
- name: TERM
value: xterm
- name: HOSTNAME
- name: container
value: oci
- name: JDBC_MARIADB_VERSION
value: 2.5.4
- name: DB_DATABASE
value: keycloak
- name: DB_USER
value: keycloak
- name: JDBC_POSTGRES_VERSION
value: 42.2.5
- name: JDBC_MSSQL_VERSION
value: 7.4.1.jre11
- name: LAUNCH_JBOSS_IN_BACKGROUND
value: "1"
- name: PROXY_ADDRESS_FORWARDING
value: "false"
- name: JDBC_MYSQL_VERSION
value: 8.0.19
- name: JBOSS_HOME
value: /opt/jboss/keycloak
- name: LANG
value: en_US.UTF-8
- name: KEYCLOAK_VERSION
value: 10.0.2
- name: DB_VENDOR
value: POSTGRES
- name: DB_ADDR
value: postgres
- name: DB_SCHEMA
value: public
- name: DB_PASSWORD
value: password
- name: KEYCLOAK_USER
value: admin
- name: KEYCLOAK_PASSWORD
value: Pa55w0rd
image: quay.io/keycloak/keycloak:latest
name: docker-compose-exampleskeycloak1
ports:
- containerPort: 8080
hostPort: 8080
protocol: TCP
resources: {}
securityContext:
allowPrivilegeEscalation: true
capabilities: {}
privileged: false
readOnlyRootFilesystem: false
runAsUser: 1000
workingDir: /
- command:
- postgres
env:
- name: PATH
value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/12/bin
- name: TERM
value: xterm
- name: HOSTNAME
- name: container
value: podman
- name: LANG
value: en_US.utf8
- name: POSTGRES_DB
value: keycloak
- name: POSTGRES_PASSWORD
value: password
- name: GOSU_VERSION
value: "1.12"
- name: PG_MAJOR
value: "12"
- name: PG_VERSION
value: 12.3-1.pgdg100+1
- name: PGDATA
value: /var/lib/postgresql/data
- name: POSTGRES_USER
value: keycloak
image: docker.io/library/postgres:latest
name: docker-compose-examplespostgres1
resources: {}
securityContext:
allowPrivilegeEscalation: true
capabilities: {}
privileged: false
readOnlyRootFilesystem: false
workingDir: /
name: root-podman-compose-keycloak-containers-docker-compose-examples-certs
status: {}
読める、読めるぞ!!
意外なくらい docker-compose 用の yml ファイルそのまんまです。
env
段落 で dockerfile 内で指定されていた環境変数のデフォルト値が表に出たので行数が嵩んでいて多少読みづらいですが、dockerfile にも keycloak-postgres.yml にもなかった設定値と言えば精々以下ぐらいではないでしょうか。
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2020-07-03T02:38:36Z"
labels:
app: docker-compose-examples
name: docker-compose-examples
securityContext:
allowPrivilegeEscalation: true
capabilities: {}
privileged: false
readOnlyRootFilesystem: false
workingDir: /
特に重要そうなセキュリティ関連についてちょっと掘り下げておきます。
allowPrivilegeEscalation
子プロセスが親プロセスより多くの特権を持つことを許可するか設定します。
capabilities
capabilities とは root が持つ特権を細分化してフラグとして管理する仕組みです。
何かしら追加の許可/不許可を行った場合リストに追加されることになるでしょう。
privileged
特権コンテナとして動作させるか否か。
ホスト上の root と同等の特権を与えることになるので注意が必要です。
readOnlyRootFilesystem
読んで字のごとく、コンテナ内の rootFS を読み取り専用にするか設定します。
大体どれも特に目的が無い限りデフォルト値で良さそうです。
さて話を戻しますが、これで生成した Kubernetes YAML を使ってポッドを作成すれば Podman の標準機能だけで複数コンテナの起動、実行、停止が完結できそうですね!
ところがどっこい……疎通取れません……!
この自動生成ファイルを podman play kube kubernetes-keycloak-postgres.yml
コマンドで読み込ませると、 Podman が同様のポッドを作成してくれますが、意図した通りに動きません。
ブラウザから 8080 ポートへアクセスした途端 Keycloak コンテナが落ちてしまいました。
# podman container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02b361d4d79c k8s.gcr.io/pause:3.2 4 minutes ago Up 4 minutes ago 0.0.0.0:8080->8080/tcp 26779078cd6d-infra
73dc25cdfd2f docker.io/library/postgres:latest docker-entrypoint... 4 minutes ago Up 4 minutes ago 0.0.0.0:8080->8080/tcp docker-compose-examplepostgres1
podman logs
で Keycloak コンテナの実行ログを見てみると PSQLException と UnknownHostException が。
あとコンテナの名前を取得するときアンダーバーが混ざっていると無視されちゃうみたいですね。
# podman logs docker-compose-exampleskeycloak1
Caused by: org.postgresql.util.PSQLException: The connection attempt failed.
at org.postgresql.jdbc@42.2.5//org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:292)
at org.postgresql.jdbc@42.2.5//org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:49)
at org.postgresql.jdbc@42.2.5//org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:195)
at org.postgresql.jdbc@42.2.5//org.postgresql.Driver.makeConnection(Driver.java:454)
at org.postgresql.jdbc@42.2.5//org.postgresql.Driver.connect(Driver.java:256)
at org.jboss.ironjacamar.jdbcadapters@1.4.20.Final//org.jboss.jca.adapters.jdbc.local.LocalManagedConnectionFactory.createLocalManagedConnection(LocalManagedConnectionFactory.java:321)
... 57 more
Caused by: java.net.UnknownHostException: postgres
at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:220)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)
at java.base/java.net.Socket.connect(Socket.java:609)
at org.postgresql.jdbc@42.2.5//org.postgresql.core.PGStream.<init>(PGStream.java:70)
at org.postgresql.jdbc@42.2.5//org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:91)
at org.postgresql.jdbc@42.2.5//org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:192)
... 62 more
これらの例外について調べてみると、どうも Keycloak コンテナが PostgreSQL コンテナとの疎通が取れなかった時に起こり得る現象のようです。java.net.UnknownHostException: postgres
ですから、字面から察するに postgres を名前解決できずに起きているのではないでしょうか。そういえば Docker-Compose によって立ち上げられたコンテナ群は、お互いにコンテナ名で疎通が取れたはずです。例外が起きた原因がコンテナ名で名前解決して疎通が取れなかったことだとすると、同様の例外が起きなかった Podman-Compose ではコンテナ名で疎通が取れていたことになります。 Podman-Compose では行われていた、コンテナ名疎通に相当する何かが Kubernetes YAML ファイルから漏れたのでは?
そう、例えば……ポッドでは基本的にコンテナ同士が仮想 NIC を共有しているので、コンテナ名を 127.0.0.1 として解決する設定を podman-compose up
`コマンドによってまとめて行われているコンテナ作成時に施している、とか……
podman run --name=service_keycloak_1 -d --pod=service
(中略)
--add-host postgres:127.0.0.1
--add-host service_postgres_1:127.0.0.1
--add-host keycloak:127.0.0.1
--add-host service_keycloak_1:127.0.0.1
quay.io/keycloak/keycloak:latest
ありました!これです。 --add-host
です。
Podman-Compose ではコンテナの作成時に --add-host
オプションでコンテナ内の /etc/hosts
にコンテナ名を 127.0.0.1
として解決する設定を書き加えています。加えて言うと、この追記は /etc/hosts
の内容はホスト上の同ファイルをコンテナ内にコピーしてから行っているので、ホストの /etc/hosts
に以下のような内容を書き込んだ上でポッドを実行すれば例外は起きなくなるはずです。
127.0.0.1 postgres
127.0.0.1 docker-compose-examples_postgres_1
127.0.0.1 keycloak
127.0.0.1 docker-compose-examples_keycloak_1
127.0.0.1 postgres
127.0.0.1 docker-compose-examples_postgres_1
127.0.0.1 keycloak
127.0.0.1 docker-compose-examples_keycloak_1
今度はブラウザからコンテナへアクセスしても Keycloak コンテナが落ちなくなりました。
# podman container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
57c8f4dbb592 quay.io/keycloak/keycloak:latest -b 0.0.0.0 41 seconds ago Up 40 seconds ago 0.0.0.0:8080->8080/tcp service_keycloak_1
725bdb94bd26 k8s.gcr.io/pause:3.2 45 seconds ago Up 43 seconds ago 0.0.0.0:8080->8080/tcp e12953ca8ff5-infra
d11f2948ef3a docker.io/library/postgres:latest postgres 44 seconds ago Up 42 seconds ago 0.0.0.0:8080->8080/tcp service_postgres_1
2 章のまとめ
-
Podman generate kube
によって出力される Kubernetes YAML は、dockerfile と docker-compose.yml 両方に記述した設定が書き出される。 - 単一のポッドについて出力させる程度だとあまり
docker-compose.yml
と大差なし。ただし若干読みづらい。 - コンテナ名で疎通が取れる前提で構築されたイメージを利用してポッドを構築する場合、同ポッド内のコンテナ名を 127.0.0.1 として解決するようコンテナ内の
/etc/hosts
に追記が必要な場合がある。
おわりに
Podman の Kubernetes YAML ファイルの解釈について更に言うと、dockerfile で言うところの ENTRYPOINT に相当するはずの command パラメータを CMD として解釈しているような挙動がある(ver.2.0.2で確認) とかまだあるのですが、これについてはまた別の記事で。Podman の Kubernetes YAML のインポート / エクスポートもまたどうやら発展途上の段階にあり、複数コンテナの連携についてはしばらくの間不便な状態が続きそうです。
参考
Balázs Németh 氏によって、 docker-compose サービスを pod に変換する意義と、シェルスクリプトと Podman CLI を使って変換する方法について解説されている記事が podman.io で紹介されていました。こちらも参考になると思います。
Convert docker-compose services to pods with Podman
https://balagetech.com/convert-docker-compose-services-to-pods/