[kubectl編] Server-Side Applyを試す

# はじめに
Kubernetes 1.22でGAされたServer-Side Apply[^1] (以降SSAと記載)について、2回に渡って投稿します。SSAはkubectlで実行するパターンと、controller-runtimeを使ってコード上で実行するパターンがあります。controller-runtimeでControllerを実装してSSAするのが一般的であるため、まずは概念理解のためにkubectlパターンを確認していきます。最終的にはコードでSSAするのを目標にします。

# SSAとは
まず、Applyについて説明しますと、KubernetesではクライアントサイドのApply(CSA)とサーバーサイドのApply(SSA)があります。SSA登場前はCSAでのApplyが広く使われており、kubectl applyコマンドでリソースを作成したまたは変更を適用した場合、Kubernetesは`kubectl.kubernetes.io/last-applied-configuration` というannotationにオブジェクトの情報を保持していました。そして、次回Apply時は、対象オブジェクトをGETし、このannotationを読み込んで差分計算後、適用を行うといったクライアントサイドでの差分計算を行う流れとなっていました。[^2] [^3]ただ、誰かがFieldを更新した後に、他の誰かが次回kubectl applyした際は、Fieldのデグレを起こしてしまう更新ができてしまうといった問題がありました。そこで、SSAの登場です。SSAはFieldに所有者(manager)を持たせ、そのFieldの更新を所有者のみにしか許可しないといったことを行い、差分計算をし易くした機能です。仮に所有者以外が更新を行うとコンフリクトが起こるようになっています。
```
error: Apply failed with 1 conflict: conflict with "junya": .spec.containers[name="test"].image
Please review the fields above--they currently have other managers. Here
are the ways you can resolve this warning:
* If you intend to manage all of these fields, please re-run the apply
  command with the `--force-conflicts` flag.
* If you do not intend to manage all of the fields, please edit your
  manifest to remove references to the fields that should keep their
  current managers.
* You may co-own fields by updating your manifest to match the existing
  value; in this case, you'll become the manager if the other manager(s)
  stop managing the field (remove it from their configuration).
See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
```
SSAを利用することで誰かにFieldを更新され、デグレを起こすといった問題をなくすことが可能となります。

# kubectlでのSSAパターン
まず、以下のようになyamlファイルを用意します。
```yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    app: test
  name: test
spec:
  containers:
  - image: nginx
    name: test
```

その後、以下コマンドでServer-Side Applyを手動で行います。
`--server-side`がServer-Side Applyでのリソース適用、
`--field-manager`がFieldの所有者
を表しています。
```
$ kubectl apply -f test.yaml --server-side --field-manager=my-manager
pod/test serverside-applied
```

apply後は`--show-managed-fields=true`フラグを立てることでFieldの所有者を確認可能です。
各Fieldのプレフィックスは[ここ](https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/types.go#L1265-L1280
)から確認してください。
```
$ kubectl get pod test -oyaml --show-managed-fields=true
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2022-07-18T12:08:57Z"
  labels:
    app: test
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:labels:
          f:app: {}
      f:spec:
        f:containers:
          k:{"name":"test"}:
            .: {}
            f:image: {}
            f:name: {}
    manager: my-manager
    operation: Apply
    time: "2022-07-18T12:08:57Z"
```
上記の場合だと、my-managerという所有者が、適用したyamlの全Filedを保持していることを表しています。
ちなみに、CSAの場合は以下のようにannotationに情報を保持します。
```
$ kubectl get pod -oyaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"test"},"name":"test","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"test"}]}}
```
仮に別の所有者を指定してlabelを更新してみましょう。
label fieldはmy-managerが所有しているのでコンフリクトが発生します。ただ、その後の取り得るアクションを提示してくれるのは非常に親切です。
```
$ cat << EOT | kubectl apply --server-side --field-manager=updater -f - 
> apiVersion: v1
> kind: Pod
> metadata:
>   creationTimestamp: null
>   labels:
>     app: test-update
>   name: test
> spec:
>   containers:
>   - image: nginx
>     name: test
> EOT
error: Apply failed with 1 conflict: conflict with "my-manager": .metadata.labels.app
Please review the fields above--they currently have other managers. Here
are the ways you can resolve this warning:
* If you intend to manage all of these fields, please re-run the apply
  command with the `--force-conflicts` flag.
* If you do not intend to manage all of the fields, please edit your
  manifest to remove references to the fields that should keep their
  current managers.
* You may co-own fields by updating your manifest to match the existing
  value; in this case, you'll become the manager if the other manager(s)
  stop managing the field (remove it from their configuration).
See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
```

アドバイス通りにしてみましょう。
--force-conflictsを指定することでCSA同じ動作にし、コンフリクトを無視することができます。
```
$ cat << EOT | kubectl apply --server-side --field-manager=updater --force-conflicts -f - 
> apiVersion: v1
> kind: Pod
> metadata:
>   creationTimestamp: null
>   labels:
>     app: test-update
>   name: test
> spec:
>   containers:
>   - image: nginx
>     name: test
> EOT
pod/test serverside-applied
```

その他、全く同じyamlを適用することでFieldの共同所有者になることも可能です。
```
$ kubectl apply -f test.yaml --server-side --field-manager=my-updater
$ kubectl get pod -oyaml --show-managed-fields=true
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    creationTimestamp: "2022-07-18T12:36:10Z"
    labels:
      app: test
    managedFields:
    - apiVersion: v1
      fieldsType: FieldsV1
      fieldsV1:
        f:metadata:
          f:labels:
            f:app: {}
        f:spec:
          f:containers:
            k:{"name":"test"}:
              .: {}
              f:image: {}
              f:name: {}
      manager: my-manager
      operation: Apply
      time: "2022-07-18T12:36:10Z"
    - apiVersion: v1
      fieldsType: FieldsV1
      fieldsV1:
        f:metadata:
          f:labels:
            f:app: {}
        f:spec:
          f:containers:
            k:{"name":"test"}:
              .: {}
              f:image: {}
              f:name: {}
      manager: my-updater
      operation: Apply
      time: "2022-07-18T12:36:25Z
```

その後、最初の所有者のoptionalなfieldを削除してapplyすることで所有権を放棄することも可能となります。今回の場合.metadata.labels fieldを削除してapplyしています。
```
$ cat test-update-lable.yaml 
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  name: test
spec:
  containers:
  - image: nginx
    name: test
$
$ kubectl apply -f test-update-lable.yaml --server-side --field-manager=my-manager
pod/test serverside-applied
$ kubectl get po -oyaml --show-managed-fields=true
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    creationTimestamp: "2022-07-18T12:36:10Z"
    labels:
      app: test
    managedFields:
    - apiVersion: v1
      fieldsType: FieldsV1
      fieldsV1:
        f:metadata:
          f:labels:
            f:app: {}
        f:spec:
          f:containers:
            k:{"name":"test"}:
              .: {}
              f:image: {}
              f:name: {}
      manager: my-updater
      operation: Apply
      time: "2022-07-18T12:36:25Z"
    - apiVersion: v1
      fieldsType: FieldsV1
      fieldsV1:
        f:spec:
          f:containers:
            k:{"name":"test"}:
              .: {}
              f:image: {}
              f:name: {}
      manager: my-manager
      operation: Apply
      time: "2022-07-18T12:45:53Z"
```

# ユーザ側でSSAを意識する必要はあるのか?
ありません。

# さいごに
今回はSSAを手動で試しました。
ユーザ側でのSSAの意識は不要ですが、Controllerで実装することで差分計算やkube-apiseverへのアクセス回数の削減などメリットがあります。現状SSA実装の公式ドキュメントは少ないですが、次回からは今回と同様の動作をControllerでの実現を目指します。

[^1]:https://kubernetes.io/docs/reference/using-api/server-side-apply/
[^2]:https://qiita.com/superbrothers/items/aeba9406691388b6a19e#%E9%81%A9%E7%94%A8-apply-%E3%81%A8%E3%81%AF%E3%81%AA%E3%81%AB%E3%81%8B
[^3]:今でもデフォルトはCSAです。