Carvel tools/kapp の特徴について説明してみました

# kappについて

[VMware Tanzu Application Platform](https://tanzu.vmware.com/content/blog/announcing-vmware-tanzu-application-platform) のベータリリースが発表されました。
Kubernetesの運用を簡素化する製品の1つとして注目していきたいです。今回はインストール時に利用されている [kapp](https://carvel.dev/kapp) について、説明いたします。

## kappとは

[Carvel tools](https://carvel.dev) の1つです。その中でkappは、***Kubernetesアプリケーション*** をコンセプトに作られたCLIです。同じラベルのリソースのセットをアプリケーションのように一括で管理できます。リソースの差分、ラベル付け、展開、および削除といった機能を備えています。

## インストール方法

OSごとの [インストール手順](https://carvel.dev/kapp/docs/latest/install/) を参考に実施してください。
また、この記事では [minikube](https://minikube.sigs.k8s.io/docs/) を利用して動作確認を行っています。

## 特徴

kappの特徴として、[ドキュメント](https://carvel.dev/kapp/docs/latest/) には、***Diff stage*** と、 ***Apply stage*** という内容でまとめられています。

## Diff stage

`kapp deploy`を実行する際に、ファイルで指定されたリソースをKubernetesAPIに存在するリソースと比較します。その際、[アノテーション(Annotations)](https://kubernetes.io/ja/docs/concepts/overview/working-with-objects/annotations/) である`kapp.k14s.io/versioned`を利用することで、リソースをバージョン管理できます。
また、バージョン管理されたリソースが更新されると既存のリソースが新しく作成されます。
バージョン管理したいリソースに、アノテーションとして、`kapp.k14s.io/versioned`と値として空文字列を設定するだけで、リソースのバージョン管理ができます。

実際に試してみます。下記のようなConfigMapを備えた`nginx.yaml`を作成します。

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: test-config
      annotations:
        kapp.k14s.io/versioned: ""
    data:
      config-value: init-config
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment
      labels:
        app: nginx
    spec:
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
            - name: nginx
              image: nginx:stable
              ports:
                - containerPort: 8080
              env:
                - name: TEST_CONFIG
                  valueFrom:
                    configMapKeyRef:
                      name: test-config
                      key: config-value

`kapp`コマンドを使ってdeploymentを作成して、pod内の環境変数`TEST_CONFIG`を確認してみます。

    $ kapp deploy -a nginx-sample -f ./nginx.yaml
    Target cluster 'https://127.0.0.1:49154' (nodes: minikube)
    
    Changes
    
    Namespace  Name               Kind        Conds.  Age  Op      Op st.  Wait to    Rs  Ri
    default    nginx-deployment   Deployment  -       -    create  -       reconcile  -   -
    ^          test-config-ver-1  ConfigMap   -       -    create  -       reconcile  -   -
    
    Op:      2 create, 0 delete, 0 update, 0 noop
    Wait to: 2 reconcile, 0 delete, 0 noop
    
    Continue? [yN]: y
    
    10:17:55AM: ---- applying 1 changes [0/2 done] ----
    10:17:55AM: create configmap/test-config-ver-1 (v1) namespace: default
    10:17:55AM: ---- waiting on 1 changes [0/2 done] ----
    10:17:55AM: ok: reconcile configmap/test-config-ver-1 (v1) namespace: default
    10:17:55AM: ---- applying 1 changes [1/2 done] ----
    10:17:55AM: create deployment/nginx-deployment (apps/v1) namespace: default
    10:17:55AM: ---- waiting on 1 changes [1/2 done] ----
    10:17:55AM: ongoing: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:17:55AM:  ^ Waiting for generation 2 to be observed
    10:17:55AM:  L ok: waiting on replicaset/nginx-deployment-85496bcc7b (apps/v1) namespace: default
    10:17:55AM:  L ongoing: waiting on pod/nginx-deployment-85496bcc7b-2g5lm (v1) namespace: default
    10:17:55AM:     ^ Pending: ContainerCreating
    10:17:56AM: ongoing: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:17:56AM:  ^ Waiting for 1 unavailable replicas
    10:17:56AM:  L ok: waiting on replicaset/nginx-deployment-85496bcc7b (apps/v1) namespace: default
    10:17:56AM:  L ongoing: waiting on pod/nginx-deployment-85496bcc7b-2g5lm (v1) namespace: default
    10:17:56AM:     ^ Pending: ContainerCreating
    10:18:15AM: ok: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:18:15AM: ---- applying complete [2/2 done] ----
    10:18:15AM: ---- waiting complete [2/2 done] ----
    
    Succeeded


成功したので、`kubectl exec`にて環境変数がどうなっているか確認します。

    $ kubectl exec -it $(kubectl get pods -l app=nginx -o json | jq -r .items[0].metadata.name) -- env | grep TEST_CONFIG
    TEST_CONFIG=init-config

ConfigMapで作成した値が環境変数`TEST_CONFIG`の値として設定されています。
次は、先ほど作成した`nginx.yaml`のConfigMapの値の部分だけ修正します。

    data:
      config-value: update-config

再度、`kapp deploy`を実行します。

    $ kapp deploy -a nginx-sample -f ./nginx.yaml
    Target cluster 'https://127.0.0.1:49154' (nodes: minikube)
    
    Changes
    
    Namespace  Name               Kind        Conds.  Age  Op      Op st.  Wait to    Rs  Ri
    default    nginx-deployment   Deployment  2/2 t   30m  update  -       reconcile  ok  -
    ^          test-config-ver-2  ConfigMap   -       -    create  -       reconcile  -   -
    
    Op:      1 create, 0 delete, 1 update, 0 noop
    Wait to: 2 reconcile, 0 delete, 0 noop
    
    Continue? [yN]: y
    
    10:47:59AM: ---- applying 1 changes [0/2 done] ----
    10:47:59AM: create configmap/test-config-ver-2 (v1) namespace: default
    10:47:59AM: ---- waiting on 1 changes [0/2 done] ----
    10:47:59AM: ok: reconcile configmap/test-config-ver-2 (v1) namespace: default
    10:47:59AM: ---- applying 1 changes [1/2 done] ----
    10:47:59AM: update deployment/nginx-deployment (apps/v1) namespace: default
    10:47:59AM: ---- waiting on 1 changes [1/2 done] ----
    10:47:59AM: ongoing: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:47:59AM:  ^ Waiting for generation 4 to be observed
    10:47:59AM:  L ok: waiting on replicaset/nginx-deployment-85496bcc7b (apps/v1) namespace: default
    10:47:59AM:  L ok: waiting on replicaset/nginx-deployment-5974d9b797 (apps/v1) namespace: default
    10:47:59AM:  L ok: waiting on pod/nginx-deployment-85496bcc7b-2g5lm (v1) namespace: default
    10:47:59AM:  L ongoing: waiting on pod/nginx-deployment-5974d9b797-cjs7v (v1) namespace: default
    10:47:59AM:     ^ Pending
    10:48:01AM: ongoing: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:48:01AM:  ^ Waiting for 1 unavailable replicas
    10:48:01AM:  L ok: waiting on replicaset/nginx-deployment-85496bcc7b (apps/v1) namespace: default
    10:48:01AM:  L ok: waiting on replicaset/nginx-deployment-5974d9b797 (apps/v1) namespace: default
    10:48:01AM:  L ok: waiting on pod/nginx-deployment-85496bcc7b-2g5lm (v1) namespace: default
    10:48:01AM:  L ongoing: waiting on pod/nginx-deployment-5974d9b797-cjs7v (v1) namespace: default
    10:48:01AM:     ^ Pending: ContainerCreating
    10:48:02AM: ok: reconcile deployment/nginx-deployment (apps/v1) namespace: default
    10:48:02AM: ---- applying complete [2/2 done] ----
    10:48:02AM: ---- waiting complete [2/2 done] ----
    
    Succeeded

ConfigMapは新しいバージョンとして作成され、deploymentも更新されていることが確認できます。
`kubectl exec`にて環境変数`TEST_CONFIG`がどうなっているか確認します。

    $ kubectl exec -it $(kubectl get pods -l app=nginx -o json | jq -r .items[0].metadata.name) -- env | grep TEST_CONFIG
    TEST_CONFIG=update-config

pod内の環境変数 `TEST_CONFIG` が変更されていることが確認できました。

`kapp deploy`の実行時に、`Op`と`Wait to`が表示されます。
`Op` は差分の概要がわかる列で、`create`, `update`, `delete`, `noop`が表示されます。
`Wait to`はどのような条件で待機するのかをしますものです`reconcile`だとリソースが目的の状態になるまで待機します。
`delete`は、削除されるまで待機します。 `noop`は、待機の条件なしとなります。

## Apply stage

Apply stageでは、以下に示すような特徴が2つあります。これらについて説明していきます。

* Apply順序の制御
* Applyの待機

### Apply順序の制御

kappには、特定の順序で変更が行われるためのアノテーションである `kapp.k14s.io/change-group`や`kapp.k14s.io/change-rule` が組み込まれています。
`kapp.k14s.io/change-group`は、リソース変更を任意の名前のグループとしてまとめることができます。
`kapp.k14s.io/change-rule`は、リソースの変更ルールを設定できます。`change-group`と併用することで、グループごとに変更の順番を決めることができます。

Apply順序を試すRedisの [サンプル](https://github.com/vmware-tanzu/carvel-kapp/tree/develop/examples/resource-ordering) がありますので、こちらを使って試してみます。サンプルは、下記の順序で構築していきます。

1. プライマリのRedisの構築
1. レプリカのRedisの構築
1. プライマリとレプリカの同期チェック

それでは、実際に試してみます。サンプルソースを取得して、manifestがある対象のディレクトリに対して、`kapp deploy`を実施します。

    $ kapp deploy -a redis-ordering -f ./resource-ordering
    Target cluster 'https://127.0.0.1:49154' (nodes: minikube)
    
    Changes
    
    Namespace  Name           Kind        Conds.  Age  Op      Op st.  Wait to    Rs  Ri
    default    redis-primary  Deployment  -       -    create  -       reconcile  -   -
    ^          redis-primary  Service     -       -    create  -       reconcile  -   -
    ^          redis-replica  Deployment  -       -    create  -       reconcile  -   -
    ^          redis-replica  Service     -       -    create  -       reconcile  -   -
    ^          sync-check     Job         -       -    create  -       reconcile  -   -
    
    Op:      5 create, 0 delete, 0 update, 0 noop
    Wait to: 5 reconcile, 0 delete, 0 noop
    
    Continue? [yN]: y
    
    11:00:23AM: ---- applying 3 changes [0/5 done] ----
    11:00:23AM: create deployment/redis-primary (apps/v1) namespace: default
    11:00:23AM: create service/redis-replica (v1) namespace: default
    11:00:23AM: create service/redis-primary (v1) namespace: default
    11:00:23AM: ---- waiting on 3 changes [0/5 done] ----
    11:00:23AM: ok: reconcile service/redis-replica (v1) namespace: default
    11:00:23AM: ok: reconcile service/redis-primary (v1) namespace: default
    11:00:23AM: ongoing: reconcile deployment/redis-primary (apps/v1) namespace: default
    11:00:23AM:  ^ Waiting for generation 2 to be observed
    11:00:23AM:  L ok: waiting on replicaset/redis-primary-64df9bf4dc (apps/v1) namespace: default
    11:00:23AM:  L ongoing: waiting on pod/redis-primary-64df9bf4dc-djg8l (v1) namespace: default
    11:00:23AM:     ^ Pending
    11:00:23AM: ---- waiting on 1 changes [2/5 done] ----
    11:00:23AM: ongoing: reconcile deployment/redis-primary (apps/v1) namespace: default
    11:00:23AM:  ^ Waiting for 1 unavailable replicas
    11:00:23AM:  L ok: waiting on replicaset/redis-primary-64df9bf4dc (apps/v1) namespace: default
    11:00:23AM:  L ongoing: waiting on pod/redis-primary-64df9bf4dc-djg8l (v1) namespace: default
    11:00:23AM:     ^ Pending: ContainerCreating
    11:00:33AM: ok: reconcile deployment/redis-primary (apps/v1) namespace: default
    11:00:33AM: ---- applying 1 changes [3/5 done] ----
    11:00:34AM: create deployment/redis-replica (apps/v1) namespace: default
    11:00:34AM: ---- waiting on 1 changes [3/5 done] ----
    11:00:34AM: ongoing: reconcile deployment/redis-replica (apps/v1) namespace: default
    11:00:34AM:  ^ Waiting for generation 2 to be observed
    11:00:34AM:  L ok: waiting on replicaset/redis-replica-55c45464b8 (apps/v1) namespace: default
    11:00:34AM:  L ongoing: waiting on pod/redis-replica-55c45464b8-srb4b (v1) namespace: default
    11:00:34AM:     ^ Pending: ContainerCreating
    11:00:34AM:  L ongoing: waiting on pod/redis-replica-55c45464b8-rj8pj (v1) namespace: default
    11:00:34AM:     ^ Pending: ContainerCreating
    ....<省略>
    11:00:41AM: ongoing: reconcile deployment/redis-replica (apps/v1) namespace: default
    11:00:41AM:  ^ Waiting for 1 unavailable replicas
    11:00:41AM:  L ok: waiting on replicaset/redis-replica-55c45464b8 (apps/v1) namespace: default
    11:00:41AM:  L ok: waiting on pod/redis-replica-55c45464b8-srb4b (v1) namespace: default
    11:00:41AM:  L ongoing: waiting on pod/redis-replica-55c45464b8-rj8pj (v1) namespace: default
    11:00:41AM:     ^ Condition Ready is not True (False)
    11:00:42AM: ok: reconcile deployment/redis-replica (apps/v1) namespace: default
    11:00:42AM: ---- applying 1 changes [4/5 done] ----
    11:00:42AM: create job/sync-check (batch/v1) namespace: default
    11:00:42AM: ---- waiting on 1 changes [4/5 done] ----
    logs | # waiting for 'sync-check--1-5wn94 > sync-check' logs to become available...
    11:00:42AM: ongoing: reconcile job/sync-check (batch/v1) namespace: default
    11:00:42AM:  ^ Waiting to complete (0 active, 0 failed, 0 succeeded)
    11:00:42AM:  L ongoing: waiting on pod/sync-check--1-5wn94 (v1) namespace: default
    11:00:42AM:     ^ Pending
    11:00:43AM: ongoing: reconcile job/sync-check (batch/v1) namespace: default
    11:00:43AM:  ^ Waiting to complete (1 active, 0 failed, 0 succeeded)
    11:00:43AM:  L ongoing: waiting on pod/sync-check--1-5wn94 (v1) namespace: default
    11:00:43AM:     ^ Pending: ContainerCreating
    ....<省略>
    11:00:49AM: ok: reconcile job/sync-check (batch/v1) namespace: default
    11:00:49AM:  ^ Completed
    11:00:49AM: ---- applying complete [5/5 done] ----
    11:00:49AM: ---- waiting complete [5/5 done] ----
    
    Succeeded

`kapp deploy`で出力された結果を見る限り、`change-group`や `change-rule`の指定した順番通りに実行されています。

### Applyの待機

リソースタイプごとに待機動作に関するルールが組み込まれています。
[サンプル](https://github.com/vmware-tanzu/carvel-kapp/blob/develop/examples/deployment-with-custom-wait/config.yml)がありますので、`Deployment`リソースタイプの動作を確認します。
実際に試してみます。こちらも先ほど同様にサンプルソースを取得して、manifestがある対象のディレクトリに対して、`kapp deploy`を実施します。


    $ kapp deploy -a custom-wait -f ./deployment-with-custom-wait
    Target cluster 'https://127.0.0.1:49154' (nodes: minikube)
    
    Changes
    
    Namespace  Name                         Kind        Conds.  Age  Op      Op st.  Wait to    Rs  Ri
    default    deployment-with-custom-wait  Deployment  -       -    create  -       reconcile  -   -
    
    Op:      1 create, 0 delete, 0 update, 0 noop
    Wait to: 1 reconcile, 0 delete, 0 noop
    
    Continue? [yN]: y
    
    11:14:55AM: ---- applying 1 changes [0/1 done] ----
    11:14:56AM: create deployment/deployment-with-custom-wait (apps/v1) namespace: default
    11:14:56AM: ---- waiting on 1 changes [0/1 done] ----
    11:14:56AM: ongoing: reconcile deployment/deployment-with-custom-wait (apps/v1) namespace: default
    11:14:56AM:  ^ Waiting for generation 2 to be observed
    11:14:56AM:  L ok: waiting on replicaset/deployment-with-custom-wait-86dcf88f5b (apps/v1) namespace: default
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-wf5jk (v1) namespace: default
    11:14:56AM:     ^ Pending
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-tkvr4 (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-rlzlm (v1) namespace: default
    11:14:56AM:     ^ Pending
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-jsxfw (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-hqhpg (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-h9vwm (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-h5n7z (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-fccnb (v1) namespace: default
    11:14:56AM:     ^ Pending
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-7wg4q (v1) namespace: default
    11:14:56AM:     ^ Pending: ContainerCreating
    11:14:56AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-5mgxg (v1) namespace: default
    11:14:56AM:     ^ Pending
    11:14:57AM: ongoing: reconcile deployment/deployment-with-custom-wait (apps/v1) namespace: default
    11:14:57AM:  ^ Waiting for at least 5 available replicas (currently 0 available)
    11:14:57AM:  L ok: waiting on replicaset/deployment-with-custom-wait-86dcf88f5b (apps/v1) namespace: default
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-wf5jk (v1) namespace: default
    11:14:57AM:     ^ Pending
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-tkvr4 (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-rlzlm (v1) namespace: default
    11:14:57AM:     ^ Pending
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-jsxfw (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-hqhpg (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-h9vwm (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-h5n7z (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-fccnb (v1) namespace: default
    11:14:57AM:     ^ Pending
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-7wg4q (v1) namespace: default
    11:14:57AM:     ^ Pending: ContainerCreating
    11:14:57AM:  L ongoing: waiting on pod/deployment-with-custom-wait-86dcf88f5b-5mgxg (v1) namespace: default
    11:14:57AM:     ^ Pending
    ....<省略>
    11:15:36AM: ok: reconcile deployment/deployment-with-custom-wait (apps/v1) namespace: default
    11:15:36AM: ---- applying complete [1/1 done] ----
    11:15:36AM: ---- waiting complete [1/1 done] ----
    
    Succeeded

サンプルでは、`apps-v1-deployment-wait-minimum-replicas-available`を50％と指定しています。
そのため、ちょっと分かりづらいですが、指定したレプリカ数の半数である5個作成時点で成功となりました。

## まとめ

kappは、kubectlなどよく使われているKubernetes運用ツールをさらに便利に利用しやすくしたものと考えています。
ただ、それだけで今まで使い慣れてきたツールを変える必要があるかと言われると、ちょっと微妙なところがあります。kapp自体、Carvel toolsの1つにすぎす、単独では価値が分かりづらいです。
[ここ](https://carvel.dev/blog/casestudy-modernizing-the-us-army/) に記載されていますように、Carvel toolsである [kapp-cotroller](https://carvel.dev/kapp-controller/), [ytt](https://carvel.dev/ytt/), [kbld](https://carvel.dev/kbld/) などを組み合わせて利用することで ***Continuous Reconciliation*** と言われているアプリケーションのデプロイ自動化がより実現しやすくなるものと考えています。