Podman で Compose したかったらどうするの？

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](https://github.com/containers/libpod#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 用の資源を流用できるツールとして大いに有用でしたが、コンテナ作成時に一部のパラメータ指定に未対応であったりと、込み入ったことをする際に困る場合があるようで、成熟にはまだ時間がかかりそうです。

https://access.redhat.com/discussions/5093741

## 流行りの 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
```

ありました。あとは [コマンドリファレンス](https://github.com/containers/podman/blob/master/docs/source/markdown/) 引いて 各オプションの意味を調べるだけです。簡単ですね。[podman pod create](https://github.com/containers/podman/blob/master/docs/source/markdown/podman-pod-create.1.md) についてはこのページです。

**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 ファイルへ変換します。

つまり、これを

```keycloak-postgres.yml
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
```

できました。

```vim: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](https://github.com/keycloak/keycloak-containers/blob/master/server/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` に以下のような内容を書き込んだ上でポッドを実行すれば例外は起きなくなるはずです。

```vim:/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](https://podman.io/) で紹介されていました。こちらも参考になると思います。
Convert docker-compose services to pods with Podman
https://balagetech.com/convert-docker-compose-services-to-pods/