Podman で Kubernetes native なアプリ開発の Innter loop から Outer loop への移行をスムーズに

これは [Red Hat Advent Calendar 2023](https://qiita.com/advent-calendar/2023/redhat) の9日目の記事です。

# TL;DR

Podman では Kubernetes マニフェストを使って Pod を実行でき、ローカル開発 -> Kubernetes クラスタへの移行がスムーズになって便利 (まだ発展途上なので今後に期待)

# 前置き

Kubernetes や OpenShift で実行するアプリケーションをローカル環境で開発するとき、どうやって開発やテストをしてますか？

多くの場合アプリケーション単体で成り立つことは少なく、データベースやメッセージブローカなどと連携したり、マイクロサービスアーキテクチャを採用している場合は分散システムを構成するサービス間での連携が必要だったりすると思います。  
個々のサービスを開発している時にシステム全体を再現して開発やテストを行いたい場合、これらの依存関係をローカル環境で再現する必要が出てきます。

たぶん一般的によくやられているのは Docker Compose や Podman Compose を使ってローカル環境にシステムを再現する方法じゃないかと思います。  
あとは Testcontainers のようなツールを使ってテスト時に一時的に必要なコンポーネントをコンテナとして立ち上げるということもできますね。

これらのツールはとても便利で冒頭の課題を解決する手法として有効だと思うのですが、あくまでローカル開発やテスト専用のツールです。  
実際の Kubernetes 環境にそのまま設定を持ち込めるわけではないので、最終的には Deployment などのマニフェストに変換する作業が必要になります。

ローカル開発環境での開発サイクル (コーディング、ビルド、実行、テスト、デバッグ) を Inner loop、その後のコードコミット、プッシュにより CI/CD により実行されるような開発サイクル (Staging/Production 用ビルド・デプロイ、テスト) を Outer loop と呼ぶことがありますが、
今回は Innner loop から Outer loop への移行をもっとスムーズにしたいということを考えてみます。どうすればいいでしょう？

![The inner loop takes place on a developer's local machine, whereas the outer loop takes place within CI/CD processes.](https://developers.redhat.com/sites/default/files/fig1_24.png)
[Kubernetes-native inner loop development with Quarkus, Figure 1: The inner loop takes place on a developer's local machine, whereas the outer loop takes place within CI/CD processes.](https://developers.redhat.com/articles/2022/12/12/kubernetes-native-inner-loop-development-quarkus#) より

ローカル環境で [Kind](https://kind.sigs.k8s.io/) や [OpenShift Local](https://access.redhat.com/documentation/ja-jp/red_hat_openshift_local/2.30/html/getting_started_guide/introducing) などを実行したり、Q[uarkus の Remote Development Mode](https://quarkus.io/guides/maven-tooling#remote-development-mode) や [Skupper](https://skupper.io/)、[Telepresence](https://www.telepresence.io/) を使用してローカルとリモート (Kubernetes クラスタ) を連携させて開発することが一つの解決策になりそうですが、今回はそれ以外の選択肢として Podman を使い、 Kubernetes マニフェストから Pod を実行する機能を使ってみたいと思います。

# 検証環境

- Red Hat Enterprise Linux 9.3
- Podman 4.6.1
- Kustomize v5.3.0

# Podman 上で Kubernetes マニフェストファイルから Pod を実行

Podman では Kubernetes マニフェストの内容を解釈し、 Podman の機能にマッピングすることで Pod を起動するための `podman kube play` コマンドが用意されています。

例えば以下のような Deployment マニフェストファイルを作ったとして、

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-service-1
  name: sample-service-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-service-1
  template:
    metadata:
      labels:
        app: sample-service-1
    spec:
      containers:
      - image: sample-service-1:latest
        imagePullPolicy: IfNotPresent
        name: sample-service-1
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        resources:
          limits:
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 500Mi
```

以下のようにコマンドを実行することで Deployment の定義をもとに Pod を実行することができます。

```bash
$ podman kube play deployment.yaml
Pod:
194467610dfe89e204b9448e95eb26a4b2afe993ca480849ab393950b174ee41
Container:
7221a08cca9b926d09293e6bb4197a08a1fd38fe13c40bffc017874ddde512df

$ podman pod ls
POD ID        NAME                         STATUS      CREATED        INFRA ID      # OF CONTAINERS
992168089ff1  sample-service-1-pod         Running     4 minutes ago  632c48d739cd  2
```

マニフェストの定義を更新したい場合は `--replace` を指定して再度 `podman kube play` することで今動いている Pod の停止と新しい Pod の実行をしてくれます。

```bash
$ podman kube play --replace sample-service-1-deployment.yaml
Pods stopped:
194467610dfe89e204b9448e95eb26a4b2afe993ca480849ab393950b174ee41
Pods removed:
194467610dfe89e204b9448e95eb26a4b2afe993ca480849ab393950b174ee41
Secrets removed:
Volumes removed:
Pod:
4b3ef8682eb096a8eaeab3647321f6732dcc52c03ca89850110406abd6fbc2ed
Container:
1cb0d1bbd6f6adced8b222eb215b3d0dff1dd2a663eedf9abbf88c4fa4eccb7f
```

検証が終わって Kubernetes マニフェストから作成した Pod やその他のリソースを停止、削除したい場合は `podman kube down` を実行します。

現時点の最新バージョンの Podman (v4.8.1) は[以下の Kubernetes マニフェストに対応しています](https://docs.podman.io/en/latest/markdown/podman-kube-play.1.html#description) (DaemonSet は v4.7.0 以降で対応)。

- Pod
- Deployment
- PersistentVolumeClaim
- ConfigMap
- Secret
- DaemonSet

実際には Podman に Deployment や DaemonSet 相当のオブジェクトはなく、それらの設定が反映された Pod およびコンテナが作成されることになります。  
PersistentVolumeClaim や ConfigMap も同様で、実際には設定を反映した Podman の Volume が作成されます (ConfigMap が環境変数として Pod に設定されている場合は Volume ではなく Pod に環境変数が反映される)。  
Secret については Podman に Secret オブジェクトがあるため、Podman の Secret として作成されます。

# 複数 Pod の実行

さて、 Kubernetes マニフェストから作成した単一の Pod を実行できましたが、アプリケーションをテストするためには依存サービスも一緒に起動したいです。  
一つの Pod 内で複数のコンテナを実行する構成も可能ですが、実際のクラスタ上ではおそらく個別の Pod として実行することが多いでしょう。

複数 Deployment のマニフェストを1つのファイルにまとめて `podman kube play` を実行することで一度に複数の Pod を実行できます。  
複数の Pod が実行できたところで、Kubernetes クラスタ上であれば Service を作成し、 Pod 間での通信を行いますが、Podman には Kubernetes の Service 相当の機能はありません。

その代わりに、 Podman では Pod 名による名前解決を可能にする DNS プラグインを使用でき、 DNS プラグインを有効化したネットワーク上で Pod を実行することで Pod 名による名前解決により通信を行えます。

DNS プラグインを有効化したネットワークは `podman kube play` による Pod の作成時に自動で `podman-default-kube-network` という名前のネットワークが作成されるようになっており、このネットワークに Pod がアタッチされます。

```bash
$ podman network inspect podman-default-kube-network 
[
     {
          "name": "podman-default-kube-network",
          "id": "e4dcdb8c6223707ebcdbac9f3fa34bdfc40792c90c87dff2cc0c4593ad1a338f",
          "driver": "bridge",
          "network_interface": "podman1",
          "created": "2023-09-19T23:56:56.649394363+09:00",
          "subnets": [
               {
                    "subnet": "10.89.0.0/24",
                    "gateway": "10.89.0.1"
               }
          ],
          "ipv6_enabled": false,
          "internal": false,
          "dns_enabled": true,
          "ipam_options": {
               "driver": "host-local"
          }
     }
]
```

# Kubernetes のマニフェスト管理ツールの利用

Kubernetes では [Kustomize](https://kustomize.io/) などのマニフェスト管理ツールを使用して環境ごとの設定を管理している場合があると思います。Podman でテストをする場合もなるべくその仕組みをそのまま使いたいですね。

例えば以下のようなディレクトリ構成で dev 環境と prod 環境の設定を Kustomize で管理しているとします。

```
├── overlays
│   ├── dev
│   │   └── kustomization.yaml
│   └── prod
│       └── kustomization.yaml
└── resources
    ├── kustomization.yaml
    ├── sample-service-1
    │   └── deployment.yaml
    ├── sample-service-2
    │   └── deployment.yaml
    └── sample-service-3
        └── deployment.yaml
```

この場合、 dev 環境のマニフェストから Pod を作成するには以下のように `kustomize build overlays/dev` で生成されるマニフェストを入力として `podman kube play --replace` を実行します。

```bash
$ kustomize build overlays/dev | podman kube play --replace -
Pod:
f82aeeeb1842697cb64baae07dff0850be03f2501cc7538447e29514095e1237
Container:
2213a7f62e93194c65c41c4cbff2f8244652724e4d166436819e1c47949223cd

Pod:
92e38e1d7a279239c27365f337608a10a75ddf8664f5ef73bce9ea39240f8f9a
Container:
3d2a8d995bf1414eaa0806eb98ca73bb6fde9f4804034bfab77d1678ef38a9b7

Pod:
96fc851b3c0dceb48709184599ad6125f7dc6ada424a1535370125caa60013e8
Container:
08f507a7e0e5f12bdea5c3292ed34bcc41dc53b949ba9326d1c2fd6b03bd72b9
```

Kubernetes のマニフェスト管理ツールをそのまま使って Pod をデプロイできました。  
実際にマニフェストの管理をする場合は Podman 上でのマニフェストの設定とクラスタ上でのマニフェストの設定にも差分があると思うので、overlays ディレクトリの下にローカルの Podman 環境用のディレクトリと kustomization.yaml を作成してもよいかもしれません。

```
├── overlays
│   ├── local
│   │   └── kustomization.yaml
│   ├── dev
│   │   └── kustomization.yaml
│   └── prod
│       └── kustomization.yaml
└── resources
    ├── kustomization.yaml
    ├── sample-service-1
    │   └── deployment.yaml
    ├── sample-service-2
    │   └── deployment.yaml
    └── sample-service-3
        └── deployment.yaml
```

こうすることでローカル (Podman)、Kubernetes クラスタ上の開発、本番環境の共通設定と個別設定をすべて Kustomize で管理できるので、Innter loop から Outer loop への移行に必要な変換作業が削減され、かつローカル環境でもマニフェストの検証ができとてもよさそうです。

# Kubernetes マニフェスト定義の Podman 機能へのマッピングと制約

Podman は Kubernetes とは異なりコンテナーオーケストレーションの機能を持たないため、 Kubernetes の完全な代替はできないことに注意が必要です。

現在対応している Kubernetes マニフェストにおいてもすべての設定に対応しているわけではなく、順次対応範囲は拡がっているものの利用できないフィールドがあることを認識しておく必要があります。対応状況は `podman kube play` コマンドのドキュメントの [Podman Kube Play Support
](https://docs.podman.io/en/latest/markdown/podman-kube-play.1.html#podman-kube-play-support) に記載があります。

また、実際には Kubernetes マニフェストの設定はその設定に相当する Podman の設定にマッピングされます。

例えば `resources.requests` および `resources.limits` の設定は以下のようにマッピングされます。

| <br>                      | コンテナの設定 <br>(podman container inspect のフィールド) | 説明                                                                                                               | podman run オプションとの対応  | 
| ------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------- | 
| resources.requests.cpu    | -                                                          |  設定が適用されない                                                                                                |  -                                | 
| resources.limits.cpu      | CpuPeriod, CpuQuota                                        | 設定にもとづき CPU 時間の制限を適用                                                                                | --cpus, --cpu-quota, --cpu-period | 
| resources.requests.memory | MemoryReservation                                          | ソフトリミット。ホストマシン上でのメモリ競合やメモリ不足を検出すると強制的に使用量をこの値に制限する                                 | --memory-reservation              | 
| resources.limits.memory   | Memory                                                     | ハードリミット。実際の制限値はOSのページサイズの倍数に丸められる。超過する場合 OOM killer によりコンテナが終了する | --memory                          | 

## 最後に

Podman の Kubernetes マニフェスト対応機能はまだまだ発展途上なので、現時点では対応リソースの種類やフィールドの制約で利用できるユースケースも限られてくるかもしれませんが、Kubernetes 上のアプリケーションの開発体験向上に一役買えるのではないかなと思います。今後の機能追加に期待 & コントリビューションしていきましょう！

## (おまけ) Podman 上の Pod やコンテナから Kubernetes マニフェストを作成

アプリケーションの開発初期にローカルの Podman 環境でしか開発しておらず、まだ Kubernetes マニフェストがない場合や、Podman Compose で動作検証してたけど Kubernetes や OpenShift クラスタ上に移行したいという場合、Podman では実行中のコンテナや Pod から Kubernetes マニフェストを生成する `podman kube generate` コマンドが用意されています。

これを使うことでマニフェストファイルの初期作成も Podman がやってくれます。

デフォルトでは Pod マニフェストが作成されますが、 `--type` オプションで `deployment` などのリソースの種類を指定することで Podman が対応している任意のリソースのマニフェストを作成できます。また、 `--service` オプションを指定すると Pod やコンテナの公開ポート情報から Service マニフェストも作成してくれます。`type: NodePort` として作成されるので適宜設定は変更してください。

```yaml
$ podman kube generate --service --type deployment sample-service-1-pod
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-4.6.1
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2023-12-08T21:58:54Z"
  labels:
    app: sample-service-1
  name: sample-service-1
spec:
  ports:
  - name: "8080"
    nodePort: 32253
    port: 8080
    targetPort: 8080
  selector:
    app: sample-service-1
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: "2023-12-08T21:58:54Z"
  labels:
    app: sample-service-1
  name: sample-service-1-deployment
spec:
  selector:
    matchLabels:
      app: sample-service-1
  template:
    metadata:
      creationTimestamp: "2023-12-08T21:58:54Z"
      labels:
        app: sample-service-1
      name: sample-service-1
    spec:
      containers:
      - image: sample-service-1:latest
        name: sample-service-1-sample-service-1
        ports:
        - containerPort: 8080
        resources:
          limits:
            memory: 1Gi
        securityContext:
          runAsNonRoot: true
      restartPolicy: Always
```

# (おまけその2) Podman 上の Pod を Kubernetes 上で実行

事前にクラスタからアクセス可能なイメージレジストリにコンテナイメージを Push しておけば、 Podman 上で起動している Pod を Kubernetes や OpenShift で実行できる `podman kube apply` コマンドも用意されています。

ただし、こちらは Deployment などの上位オブジェクトで管理されない素の Pod が実行されるので、とりあえず動かしてみたいというような目的以外では `podman kube generate` で生成したマニフェストを使って Pod をデプロイするのがよいかもしれません。

```bash
$ podman kube apply --ns innerloop2outerloop-with-podman --service sample-service-1-pod
Deploying to cluster...
Successfully deployed workloads to cluster!

$ oc get pod
NAME                       READY   STATUS              RESTARTS   AGE
sample-service-1-pod       0/1     ContainerCreating   0          2s

$ oc get service
NAME                       TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
sample-service-1-pod       NodePort   172.30.64.96   <none>        8081:30852/TCP   4m44s
```