CarvelとBOSH

この記事は、[TUNA-JP Advent Calendar 2022](https://qiita.com/advent-calendar/2022/tuna-jp) の24日目のエントリーとして、VMware がメインスポンサーとなって開発が進められているオープンソースソフトウェア群であり [Cloud Native Computing Foundation の Sandbox project のひとつ](https://carvel.dev/blog/carvel-cncf-sandbox/)である [Carvel](https://carvel.dev) について、そもそもこのようなソフトウェア群が生まれた背景を、Kubernetes 登場以前から存在するオープンソースソフトウェアプロジェクトである [Cloud Foundry BOSH](https://bosh.io/docs/) の設計思想や開発メンバーとの関わりと併せて解き明かしていこうとするものです。
ほとんどの内容が過去の経緯を振り返るものであり、目新しい内容はあまりありません。おっさんのくだらない昔話ばっかりです。ただ１点、大事なことを最後に書いときますので、お急ぎの方はそこだけ見ていってくださいませ。

# 4年前の Advent Calendar 記事を振り返る

まだ Kubernetes のことを大して知らず、転職も未経験だった私は、[『BOSHとリリースエンジニアリング』](https://qiita.com/kenojiri/items/b481317236e010185077)という記事を書いていました。

https://qiita.com/kenojiri/items/b481317236e010185077

ひとつのYAMLで仮想マシンベースの様々な分散システムを定義づけることができる BOSH というものが、確固たる理念のもとで設計されており、Cloud Foundry という非常に複雑なアプリケーションプラットフォームの構築と運用を支えていることをご紹介しました。

その後時代は流れ、今では Kubernetes をコンテナプラットフォームとしてその上でのアプリケーションの構築と運用が当たり前となりました。YAMLによってコンテナプラットフォーム上のワークロードを定義づけることもかなり当たり前になり、さらに Terraform などの Infrastructure as Code を支えるツールもいよいよ普及してきていることから、もはやBOSHを推してく世の中でもない感じになっています。
最近では、YAML や HCL などを Git レポジトリに格納して source of truth とし、その変更を契機にアプリケーションやインフラを自動的に更新してゆく継続的デリバリー(CD)のプラクティスである GitOps を、運用の省力化やワークフローの高速化の文脈で実践している皆さんも多いのではないでしょうか。

# Kubernetes とリリースエンジニアリング

[『BOSHとリリースエンジニアリング』](https://qiita.com/kenojiri/items/b481317236e010185077)の記事で、モダンなリリースエンジニアリングを考えるなら、そのへんはBOSHがよしなにやるから、いいからBOSHを使えよ！という主旨をお伝えしたところです。

では、今われわれがようやく使い慣れた Kubernetes は、BOSH によって実現されていたリリースエンジニアリングの各要素をどこまで満たしてくれているのでしょうか。

![BOSHの設計理念](https://camo.qiitausercontent.com/3293bf2b051ed7833992390b4e191c78c0faa6dd/68747470733a2f2f676973742e6769746875622e636f6d2f6f7a7a6f7a7a2f32336239343164346534363838313065643036666437356436643431336364342f7261772f736c69646531322e706e67)

一見すると、Kubernetes を使うことで、かなりの部分を満たしているように思えます。

- Kubernetes では、ひとつまたは複数のYAMLによってコンテナプラットフォーム上のワークロードを統合的に定義づけることができるので、Identifiability と Reproduciability を満足している！
    - オレたちには手塩にかけて書き上げたYAMLがある。これさえあれば、どんな Kubernetes クラスタでも、全く同じ構成でワークロードをデプロイできる！
- `kubectl apply` コマンドには冪等性があり、YAMLをうまいこと整えておきさえすれば、このコマンドだけでワークロードの新規デプロイも更新もできるので、Consistency と Agility を満足している！
    - 俺たちはもはや、`kubectl create` やら `kubectl patch` やら、そういう手続き的な運用をしていない。オレたちには手塩にかけて書き上げたYAMLがある。こいつを更新したら、必ず `kubectl apply` コマンドを使って反映させることで宣言的な運用を実現、冪等性もバッチリや！

これらは本当でしょうか？

# Kubernetes でも残るリリースエンジニアリング面の悩み

以下に示す2つのケースを考えてみましょう。どうやら Kubernetes を単に使うだけでは、うまい具合に解決しなさそうな雰囲気がします。

## その1: 切り戻しや不具合再現がうまくいかない

貴方のその手塩にかけたYAMLに、例えば以下のような記述はないでしょうか？

例1
```
  image: hoge-container-registry.com/hoge-project/hoge-app:latest
```

Pod や Deployment などのYAMLで使われるコンテナイメージの参照(ほとんどは `image:` のハッシュ(or ディクショナリー)の値として書かれる)において、例1のような `latest` タグは、とりあえずコンテナレジストリに置いてある最新のイメージを使ってデプロイできればそれでいい、動くのが確認できたらそれでOKという、動作検証の場合などには、とても便利です。

しかし、本番環境やそれに近い環境(ステージング環境、QA環境など)ではよくある話なのですが、直近にビルドしたコンテナイメージにバグが見つかったので、その前に正常動作していたコンテナイメージに戻して修正デプロイする、いわゆる「切り戻し」操作が必要になったとき、 `latest` タグを使っていると、いきなり困ることになります。コンテナレジストリのUIを眺め、リリースの直前まで動いてたイメージはどれだったかな・・・あれ、前回のリリースからだいぶ経ってて、それ以降にコンテナイメージの再ビルド・再pushがたくさん行われてて、どれが正解だか分からないぞ・・・みたいな状況。あるある、と思われた方が結構おられるのではないでしょうか。

例2
```
  image: nginx:1.23.3-alpine
```

`latest` タグだとどのバージョンのソフトウェアを使うのかが明示できないので、リリースの際には `image:` にバージョン番号をタグとして設定するお作法を採用している。そうすれば、切り戻しの際には以前正常動作していたときの同じコンテナイメージが使われるので、安心して切り戻せるはずである。大丈夫大丈夫・・・

残念。世の中そんなに甘くはありません。

`nginx:1.23.3-alpine` というコンテナイメージには nginx 1.23.3 に関連するファイルだけが含まれているわけではありません。2022/12/24時点のベースイメージである `alpine:3.17.0` には libc.musl や libssl などといった共有ライブラリも含まれており、これらにバグ改修や改善などが行われると `alpine:3.17.0` というコンテナイメージが再ビルドされ DockerHubに 再pushされて差し替わります。さらに `nginx:1.23.3-alpine` も自動的に再ビルド・再pushされて差し替わります。
すなわち、同じタグでも時が経てばコンテナイメージの中身が変わっているということは、起こり得るのです。上記の例では、共有ライブラリが差し替わることによって、nginxの動作が変わることもあり得る話です。そのため、切り戻そうと思ってもうまくいかない、不具合をなぜか再現できない、ということも起こり得ることになります。

自チームで開発しているアプリであれば、ビルド時やリリース時などにコンテナイメージにタグを適切に付与することで、切り戻しなどの際に所望のコンテナイメージを使えるため、その点で困ることはなさそうです。しかし、nginxや各種のDBMS(MySQLやPostgreSQLなど)といった、アプリに付随して動作させるようなコンテナを、外部のコンテナレジストリ(DockerHubなど)のイメージを持ち込んでデプロイする場合には、コンテナイメージに対して勝手にタグを振るわけにもいかず、困ってしまうことがあるのではないでしょうか。

### 考えられる対策

コンテナイメージの参照に、 `:latest` `:3.17.0` `:1.23.3-alpine` などのいったタグは使わず、 `index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793` などという digest 値を使う記法にします。
こうすれば、コンテナイメージ内のディレクトリツリー全てに対して一意性を担保できますので、所望の時点への切り戻しや不具合再現がうまくいくことになります。

```
  image: index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793
```

ただ、YAMLに書かれている全てのコンテナイメージの参照を digest 記法とするのは、手動で行うのはとても面倒です。
多くのコンテナからなる複雑な構成の場合、それぞれのコンテナイメージの digest をいちいち調べるのか・・・そのへん、何とか自動的によしなにやってくれないものなのか。

### BOSH ではどうしていたのか？

BOSH において、Kubernetes におけるコンテナイメージに当たるものは、強いて言えば [BOSH stemcell](https://bosh.io/docs/stemcell/) と [BOSH release](https://bosh.io/docs/create-release/) との組み合わせ、ということになります。
BOSH stemcell は VMのベースイメージであり、ビルドされた順番に沿ってバージョン番号が振られています。
BOSH release は言わば Source Tarball の形式で格納されたソフトウェアパッケージの集合体であり、それぞれのソフトウェアパッケージとしてはソースコードのバージョン番号あるいは Git commit ID などによって識別される特定のバージョンのみが格納されています。
BOSH によってデプロイされたVMに着目すると、stemcell のバージョン と release に含まれる各ソフトウェアパッケージのバージョンがありますが、stemcell バージョンと release バージョンが明記されているひとつの [BOSH deployment config](https://bosh.io/docs/manifest-v2/) からデプロイされた結果として、いずれのバージョンも必ず一意となっています。
すなわち、BOSH deployment config において BOSH stemcell と BOSH release の各バージョンを明示すれば、過去のいかなる構成も再現可能となるわけです。

## その2: ConfigMap や Secret の更新が反映されない

ConfigMap や Secret としてワークロードの設定値や設定ファイルを定義しておき、コンテナには configMapKeyRef や secretKeyRef 経由で環境変数を設定したり、ボリュームマウントして設定ファイルとしたりするようなことがよくありますね。
しかしほとんどの場合、ConfigMap や Secret のYAMLを更新して `kubectl apply` しても、その設定の変更が反映されるべき Pod が再作成されることもコンテナが再起動されることもなく、平然と変更前の設定のままで動き続けるだろうと思います。もし設定ファイルの変更を検知して勝手に設定をリロードするようなソフトウェアだったら、それはそれで便利かもしれませんが、コンテナじゃない環境で動かすような場合は逆に運用がシビアになりそうで、なかなかこわい実装だと思います。

例として、コンテナにボリュームマウントする nginx の設定ファイルを ConfigMap として設定し、これを `kubectl apply` で更新しても、その変更が効かない模様を、以下に示します。
```
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  labels:
    app: nginx
data:
  nginx.conf: |
    worker_processes 1;
    events {}
    http {
      access_log /mnt/access.log combined;
      server {
        listen 80 default_server;
        root /usr/share/nginx/html;
        index index.html;
      }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-lb
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.23.3-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx
          readOnly: true
          name: nginx-conf
      volumes:
      - name: nginx-conf
        configMap:
          name: nginx-conf
          items:
          - key: nginx.conf
            path: nginx.conf
EOF

configmap/nginx-conf created
service/nginx-lb created
deployment.apps/nginx created

$ kubectl get all -l app=nginx
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-7d8ffdbdb9-j8vn5   1/1     Running   0          13s

NAME               TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)        AGE
service/nginx-lb   LoadBalancer   10.43.82.56   192.168.1.131   80:31716/TCP   13s

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx   1/1     1            1           13s

NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-7d8ffdbdb9   1         1         1       13s

$ curl 192.168.1.131
【略】

$ k exec nginx-7d8ffdbdb9-j8vn5 -- cat /mnt/access.log
10.42.0.0 - - [24/Dec/2022:12:34:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0"

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  labels:
    app: nginx
data:
  nginx.conf: |
    worker_processes 1;
    events {}
    http {
      log_format short "time: \$time_iso8601";
      access_log /mnt/access.log short;
      server {
        listen 80 default_server;
        root /usr/share/nginx/html;
        index index.html;
      }
    }
EOF

configmap/nginx-conf configured

$ curl 192.168.1.131
【略】

$ k exec nginx-7d8ffdbdb9-j8vn5 -- cat /mnt/access.log
10.42.0.0 - - [24/Dec/2022:12:34:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0"
10.42.0.0 - - [25/Dec/2022:12:35:02 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0"
```
仕組みがよく分かっていないと、いかにも理不尽を突きつけられている雰囲気のログですが、要はnginxが新しい設定ファイルを読み込んでないのが原因です。

更新デプロイを完結させるためには、`kubectl rollout restart deployment/???` するだの、 `kubectl delete pod` して Deployment から Pod が自動再作成されるのを待つだの、ちょっとした追加コマンドを実行しないといけないことになります。

CDとかがんばってやろうとしても、その点でハマったり、そこを解決するためにCDパイプラインに変な設定を追加しないといけなかったり、いっそやむなく諦めて手運用に戻ったりなんかしたりして。

### 考えられる対策

ConfigMap や Secret を更新するたびに、そのリソースの名前(metadata.name)を新しいものに付け替えていき、その都度参照元となる Deployment などに設定する ConfigMap 名 や Secret 名を併せて書き換える、という手法があるようです。こうすることで、ConfigMap や Secret の更新が反映されるべき Deployment などにも何らかの更新がなされるため、更新デプロイがめでたく完結する、と。

うーん。泥臭い。泥臭すぎる・・・そのへん、何とか自動的によしなにやってくれないものなのか。

### BOSH ではどうしていたのか？

BOSH stemcell や BOSH release に含まれるタスク群(BOSHでは job と呼ばれる)に対する設定値は、[deployment config](https://bosh.io/docs/manifest-v2/) もしくは [director runtime config](https://bosh.io/docs/runtime-config/) に記載します。
それらの設定値を更新デプロイする際、BOSH director は diff をとって更新が必要なVMがどれかをピックアップし、該当のVMについては設定値を流し込んで全ての job を再起動(もしくは必要に応じてVMごと再作成)します。
deployment config と director runtime config のいずれも、どのVMが設定更新対象となるのかを特定しやすいようなスキーマとなっており、漏れなくムダなく設定変更を行える設計となっています。

# Kubernetes 上のリリースエンジニアリングのかゆいところに手が届く Carvel

[2020年8月に投稿された Carvel 公式のブログ記事](https://carvel.dev/blog/introduction-to-carvel-blog-post/)には、思いっきり
```
Carvel was born from frustration with existing tools
```
とか書いてあります。Kubernetes 自体は良いものだが、リリースエンジニアリング的にかゆいところには微妙に手が届かなかったし、他のツールも帯に短し襷に長しで、しょうがなく作ったのが Carvel の各ツール群だったのです。

## YAMLに書かれた全てのコンテナイメージの参照を digest 記法に塗り替える [kbld](https://carvel.dev/kbld/)

Carvel のツール群のひとつに kbld というものがあります。
例えば、以下のように使えます。
```
$ cat <<EOF | kbld -f -
apiVersion: v1
kind: Pod 
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.23.3-alpine
EOF

resolve | final: nginx:1.23.3-alpine -> index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793
---
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kbld.k14s.io/images: |
      - origins:
        - resolved:
            tag: 1.23.3-alpine
            url: nginx:1.23.3-alpine
        url: index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793
  name: nginx
spec:
  containers:
  - image: index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793
    name: nginx

Succeeded
```

元のYAMLをパイプで kbld に通せば、`image:` を拾って digest 記法に塗り替えたYAMLを出力してくれます。
GitOps を行う中で、切り戻しや不具合再現の際のトラブルを解消するためには、YAMLを Git リポジトリに格納する前に kbld を通して全てのコンテナイメージの参照を digest 記法に塗り替えておくべきでしょう。

## ワークロードの設定変更を漏らさず更新デプロイする [kapp](https://carvel.dev/kapp/docs/latest/)

Carvel のツール群のひとつに kapp というものがあります。
`kubectl apply -f` の代わりに `kapp deploy -a [app_name] -c -f` のコマンドを使うことで、変更差分をプレビューしたうえで更新デプロイできたりする便利なコマンドなのですが、実はニッチな機能も数多くもっています。
そのひとつが、 [Versioned Resources](https://carvel.dev/kapp/docs/latest/diff/#versioned-resources) というものです。

ワークロードの設定更新の際に反映させたい ConfigMap や Secret などのリソースに対して `kapp.k14s.io/versioned: ""` という annotation を追加しておくことで、元のYAMLに書いていたリソース名にバージョン番号を付加した別名をつけて実際のリソースを作成し、このリソースを参照するべき Deployment などからの参照先も勝手に別名の方に付け替えてくれます。

どういうことかよくわからないと思いますので、`kubectl apply` でうまくいかなかった nginx の ConfigMap による設定変更が kapp を使うとうまくいく模様を以下に示します。

```
$ cat <<EOF | kapp deploy -a nginx -f - -y
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  labels:
    app: nginx
  annotations:
    kapp.k14s.io/versioned: ""
data:
  nginx.conf: |
    worker_processes 1;
    events {}
    http {
      access_log /mnt/access.log combined;
      server {
        listen 80 default_server;
        root /usr/share/nginx/html;
        index index.html;
      }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-lb
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.23.3-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx
          readOnly: true
          name: nginx-conf
      volumes:
      - name: nginx-conf
        configMap:
          name: nginx-conf
          items:
          - key: nginx.conf
            path: nginx.conf
EOF

Target cluster 'https://127.0.0.1:6443' (nodes: home03, 1+)

Changes

Namespace  Name              Kind        Age  Op      Op st.  Wait to    Rs  Ri  
default    nginx             Deployment  -    create  -       reconcile  -   -  
^          nginx-conf-ver-1  ConfigMap   -    create  -       reconcile  -   -  
^          nginx-lb          Service     -    create  -       reconcile  -   -  

Op:      3 create, 0 delete, 0 update, 0 noop, 0 exists
Wait to: 3 reconcile, 0 delete, 0 noop

4:33:06PM: ---- applying 1 changes [0/3 done] ----
4:33:06PM: create configmap/nginx-conf-ver-1 (v1) namespace: default
4:33:06PM: ---- waiting on 1 changes [0/3 done] ----
4:33:06PM: ok: reconcile configmap/nginx-conf-ver-1 (v1) namespace: default
4:33:06PM: ---- applying 2 changes [1/3 done] ----
4:33:07PM: create service/nginx-lb (v1) namespace: default
4:33:07PM: create deployment/nginx (apps/v1) namespace: default
4:33:07PM: ---- waiting on 2 changes [1/3 done] ----
4:33:07PM: ok: reconcile service/nginx-lb (v1) namespace: default
4:33:07PM: ongoing: reconcile deployment/nginx (apps/v1) namespace: default
4:33:07PM:  ^ Waiting for generation 2 to be observed
4:33:07PM:  L ok: waiting on replicaset/nginx-859ffcd8c (apps/v1) namespace: default
4:33:07PM:  L ok: waiting on pod/nginx-859ffcd8c-ft64b (v1) namespace: default
4:33:07PM: ---- waiting on 1 changes [2/3 done] ----
4:33:07PM: ok: reconcile deployment/nginx (apps/v1) namespace: default
4:33:07PM: ---- applying complete [3/3 done] ----
4:33:07PM: ---- waiting complete [3/3 done] ----

Succeeded

$ kubectl get all,cm -l app=nginx
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-5667ccd54b-ll67q   1/1     Running   0          5m9s

NAME               TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
service/nginx-lb   LoadBalancer   10.43.154.165   192.168.1.131   80:31150/TCP   2m34s

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx   1/1     1            1           2m34s

NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-5667ccd54b   1         1         1       5m9s
replicaset.apps/nginx-859ffcd8c    0         0         0       8m59s

NAME                         DATA   AGE
configmap/nginx-conf-ver-1   1      2m34s

$ curl 192.168.1.131
【略】

$ k exec nginx-859ffcd8c-ft64b -- cat /mnt/access.log
10.42.0.0 - - [24/Dec/2022:12:56:02 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0"

$ cat <<EOF | kapp deploy -a nginx -f - -y
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  labels:
    app: nginx
  annotations:
    kapp.k14s.io/versioned: ""
data:
  nginx.conf: |
    worker_processes 1;
    events {}
    http {
      log_format short "time: \$time_iso8601";
      access_log /mnt/access.log short;
      server {
        listen 80 default_server;
        root /usr/share/nginx/html;
        index index.html;
      }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-lb
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.23.3-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx
          readOnly: true
          name: nginx-conf
      volumes:
      - name: nginx-conf
        configMap:
          name: nginx-conf
          items:
          - key: nginx.conf
            path: nginx.conf
EOF

Target cluster 'https://127.0.0.1:6443' (nodes: home03, 1+)

Changes

Namespace  Name              Kind        Age  Op      Op st.  Wait to    Rs  Ri  
default    nginx             Deployment  3m   update  -       reconcile  ok  -  
^          nginx-conf-ver-2  ConfigMap   -    create  -       reconcile  -   -  

Op:      1 create, 0 delete, 1 update, 0 noop, 0 exists
Wait to: 2 reconcile, 0 delete, 0 noop

4:36:56PM: ---- applying 1 changes [0/2 done] ----
4:36:56PM: create configmap/nginx-conf-ver-2 (v1) namespace: default
4:36:56PM: ---- waiting on 1 changes [0/2 done] ----
4:36:56PM: ok: reconcile configmap/nginx-conf-ver-2 (v1) namespace: default
4:36:56PM: ---- applying 1 changes [1/2 done] ----
4:36:57PM: update deployment/nginx (apps/v1) namespace: default
4:36:57PM: ---- waiting on 1 changes [1/2 done] ----
4:36:57PM: ongoing: reconcile deployment/nginx (apps/v1) namespace: default
4:36:57PM:  ^ Waiting for generation 4 to be observed
4:36:57PM:  L ok: waiting on replicaset/nginx-859ffcd8c (apps/v1) namespace: default
4:36:57PM:  L ok: waiting on replicaset/nginx-5667ccd54b (apps/v1) namespace: default
4:36:57PM:  L ok: waiting on pod/nginx-859ffcd8c-ft64b (v1) namespace: default
4:36:57PM:  L ongoing: waiting on pod/nginx-5667ccd54b-ll67q (v1) namespace: default
4:36:57PM:     ^ Pending: ContainerCreating
4:36:58PM: ok: reconcile deployment/nginx (apps/v1) namespace: default
4:36:58PM: ---- applying complete [2/2 done] ----
4:36:58PM: ---- waiting complete [2/2 done] ----

Succeeded

$ kubectl get all,cm -l app=nginx
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-5667ccd54b-ll67q   1/1     Running   0          5m9s

NAME               TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
service/nginx-lb   LoadBalancer   10.43.154.165   192.168.1.131   80:31150/TCP   8m59s

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx   1/1     1            1           8m59s

NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-5667ccd54b   1         1         1       5m9s
replicaset.apps/nginx-859ffcd8c    0         0         0       8m59s

NAME                         DATA   AGE
configmap/nginx-conf-ver-1   1      8m59s
configmap/nginx-conf-ver-2   1      5m9s

$ curl 192.168.1.131
【略】

$ k exec nginx-5667ccd54b-ll67q -- cat /mnt/access.log
time: 2022-12-24T12:57:09+00:00

$ k describe deploy nginx
Name:                   nginx
Namespace:              default
【略】
  Volumes:
   nginx-conf:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      nginx-conf-ver-2
    Optional:  false
```

あー、これは、地味に便利なやつや。
特に、GitOps で kapp (というか、むしろ kapp-controller) を使う場合は、是非とも忘れずに設定しておきたい annotation ということになります。

# Carvel と BOSH の浅からぬ関係

[Carvel プロジェクト](https://carvel.dev)に古くから参加している [Dmitriy Kalinin 氏](https://github.com/cppforlife)や [John Ryan 氏](https://github.com/pivotaljohn)は、実は Carvel 以前に Pivotal に所属して Cloud Foundry の Contributor として活躍をしてきた人たちであり、 Pivotal が VMware に買収された後も VMware Tanzu のプロダクト開発を続けています。他にも [Nima Kaviani 氏](https://github.com/nimakaviani)など、Pivotal / VMware には所属してはいないが Cloud Foundry の Contributor をされていた方も、Carvel に深く関わっておられるようです。

Cloud Foundry BOSH ではちゃんとできてたリリースエンジニアリングが、Kubernetes に移った途端にデグレードするのは忍びないというのは無理からぬことだと思います。
長年にわたって積み上げてきた BOSH の崇高な設計理念は、今や Carvel に引き継がれて Kubernetes をとりまくエコシステムにおいても生き続けていくということなんですねえ。

# 最後に一つ、大事なこと

GitOps で kapp やら kapp-controller をお使いの方はまだ少数派で、多くの方は ArgoCD、Flux、Spinnaker などで GitOps を実現されているのだろうと思います。そんな方たちにもひとつだけ是非ともお勧めしたい。

YAMLを Git リポジトリに格納する際には、全てのコンテナイメージの参照を digest 記法 (`index.docker.io/library/nginx@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1db793` の形式) に塗り替えておきましょう。

# おしまい