kubectl patch コマンドを理解する

# はじめに
実行中の deployment に後付けで volume をマウントしたくなりました。
- そもそも実行中 deployment に新規 volume アサインできるの？
- kubeclt patch コマンドで実現できるの？

等を解消していきたいと思います。

# 目的
実行中 deployment に volume を追加する。

# 手段
kubectl patch コマンドで行けると思いますが...

# 事前調査
まずは patch コマンドの仕様を理解したいと思います。

##### kubecctl patch コマンドとは？
リソースフィールドを更新するコマンドです。また、JSON/YAML形式を利用可能です。
patch コマンド実行時、対象リソースの追加 or リプレイス動作はリソースに依存して異なります。(patch strategy と呼ばれます)

patch の種類?タイプ?は三つ存在します。
1. JSON Patch
1. JSON merge patch
1. Strategic merge patch

```shell
※ kubectl patch コマンドは type パラメータで patch タイプを指定可能
# kubectl patch <type>
```

各リソースの path stragegy は [OpenApi spec](https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json)または[API OVERVIEW](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#-strong-api-overview-strong-)から確認できます。

確認方法は、`volumes` 等のキーワード検索、またはどのリソース内で指定している要素なのかを確認すると良いです。例えば、containers 内の項目であれば contaisner の項目を確認すればよいです。

なお、pathStrategy キーが存在しないリソースはデフォルト動作(replace)となります。


公式ドキュメントに従ってサンプルの deployment を作成します。
```shell:deployment-patch.yaml
cat <<EOF > deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: patch-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: patch-demo-ctr
        image: nginx
      tolerations:
      - effect: NoSchedule
        key: dedicated
        value: test-team
EOF
```

次に patch 用の yaml を作成して適用します。

```shell:patch-file.yaml
cat << EOF > patch-file.yaml
spec:
  template:
    spec:
      containers:
      - name: patch-demo-ctr-2
        image: redis
EOF
```

```shell
# kubectl patch deployment patch-demo --patch-file patch-file.yaml
deployment.apps/patch-demo patched

```

各 yaml の差分は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474478/7cc30c04-dcfa-8e0e-9b68-7f797b7516b2.png)

Pod 内のコンテナイメージ数を確認します。

```shell
kubectl get deployment patch-demo --output yaml
--- snip ---
    spec:
      containers:
      - image: redis <---- HERE
        imagePullPolicy: Always
        name: patch-demo-ctr-2
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      - image: nginx <---- HERE
        imagePullPolicy: Always
        name: patch-demo-ctr
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
--- snip ---
```

OpenApi から今回更新対象の `volumeMounts` と `volumes patch strategy` を確認します。
まず、`volumeMounts` は **merge** なので追加される動作ですね。

>        "volumeMounts": {
>          "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.",
>          "items": {
>            "$ref": "#/definitions/io.k8s.api.core.v1.VolumeMount"
>          },
>          "type": "array",
>          "x-kubernetes-patch-merge-key": "mountPath",
>          "x-kubernetes-patch-strategy": "merge"
>        },

次に、`volumes` は **merge,retainKeys** なので追加ですね。
retainKeys が何を指すのか理解できていませんが、追って確認予定です。


>"volumes": {
>          "description": "List of volumes that can be mounted by containers belonging to the pod. More info: >https://kubernetes.io/docs/concepts/storage/volumes",
>          "items": {
>            "$ref": "#/definitions/io.k8s.api.core.v1.Volume"
>          },
>          "type": "array",
>          "x-kubernetes-patch-merge-key": "name",
>          "x-kubernetes-patch-strategy": "merge,retainKeys"
>        }

追加で patch 用の yaml を作成します。

```shell:patch-file-tolerations.yaml
cat << EOF > patch-file-tolerations.yaml
spec:
  template:
    spec:
      tolerations:
      - effect: NoSchedule
        key: disktype
        value: ssd
EOF
```

作成した patch 用の yaml を適用します。

```shell
# kubectl patch deployment patch-demo --patch-file patch-file-tolerations.yaml
deployment.apps/patch-demo patched
```

merge タイプの patch コマンド用 yaml を作成します。

```shell:patch-file-2.yaml
cat << EOF > patch-file-2.yaml
spec:
  template:
    spec:
      containers:
      - name: patch-demo-ctr-3
        image: gcr.io/google-samples/node-hello:1.0
EOF
```

作成した yaml を `merge` タイプで適用します。

```shell
# kubectl patch deployment patch-demo --type merge --patch-file patch-file-2.yaml
deployment.apps/patch-demo patched
```

retainkeys を使用した patch コマンドを確認してきます。

```shell:deployment-retainkeys.yaml
cat << EOF > deployment-retainkeys.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: retainkeys-demo
spec:
  selector:
    matchLabels:
      app: nginx
  strategy:
    rollingUpdate:
      maxSurge: 30%
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: retainkeys-demo-ctr
        image: nginx
EOF
```

```shell
# kubectl apply -f deployment-retainkeys.yaml 
deployment.apps/retainkeys-demo created
```

```shell:patch-file-no-retainkeys.yaml
cat << EOF > patch-file-no-retainkeys.yaml
spec:
  strategy:
    type: Recreate
EOF
```

>        "strategy": {
>          "$ref": "#/definitions/io.k8s.api.apps.v1.DeploymentStrategy",
>          "description": "The deployment strategy to use to replace existing pods with new ones.",
>          "x-kubernetes-patch-strategy": "retainKeys"
>        },
`strategy` は retainKeys なので、`strategy` を更新する場合は retainKeys を使用する必要があります。

```shell:patch-file-retainkeys.yaml
cat << EOF > patch-file-retainkeys.yaml
spec:
  strategy:
    $retainKeys:
    - type
    type: Recreate
EOF
```

```shell
# kubectl patch deployment retainkeys-demo --type strategic --patch-file patch-file-retainkeys.yaml
deployment.apps/retainkeys-demo patched
```

また、kubectl patch コマンドは json 形式でも実行可能です。

```shell:patch-file.json
cat << EOF > patch-file.json
{
   "spec": {
      "template": {
         "spec": {
            "containers": [
               {
                  "name": "patch-demo-ctr-2",
                  "image": "redis"
               }
            ]
         }
      }
   }
}
EOF
```

```shell:
# kubectl patch deployment patch-demo --patch-file patch-file.yaml
deployment.apps/patch-demo patched
```

```shell:
kubectl patch deployment patch-demo --patch-file patch-file.yaml
kubectl patch deployment patch-demo --patch 'spec:\n template:\n  spec:\n   containers:\n   - name: patch-demo-ctr-2\n     image: redis'

kubectl patch deployment patch-demo --patch-file patch-file.json
kubectl patch deployment patch-demo --patch '{"spec": {"template": {"spec": {"containers": [{"name": "patch-demo-ctr-2","image": "redis"}]}}}}'
```


〇 参考文献
- [kubectl patch - 公式ドキュメント](https://jamesdefabia.github.io/docs/user-guide/kubectl/kubectl_patch/)
- [Update API Objects in Place Using kubectl patch - 公式ドキュメント](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)
- [OpenApi spec](https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json)


ここまでの理解を整理すると以下の通りです。

- `strategic merge patch` は k8s が良い感じにマージしてくれるやつ
- `JSON Patch` : 更新対象ではない部分も記載してあげないといけないやつ
- `JSON Merge` : 更新対象のみの記載でいけるやつ


# 動作確認
概ね patch コマンド動作は理解できたので、目的である volume 追加を動作確認します。
作業手順は以下の通りです。

- テスト用 yaml ファイル作成

```shell:patch-volume-test.yaml
cat << EOF > patch-volume-test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: patch-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: before-volume-pvc
          persistentVolumeClaim:
            claimName: before-volume
      containers:
      - name: patch-demo-ctr
        image: nginx
        volumeMounts:
          - mountPath: "/tmp/"
            name: before-volume-pvc
      tolerations:
      - effect: NoSchedule
        key: dedicated
        value: test-team
EOF
```

- nfs volume 作成

```shell
# mkdir -p before-volume
# touch before-volume/before-volume.txt
# vi /etc/exports
# exportfs -ra
```

- persistentvolume 作成

```shell:patch-persistentvolume.yaml
cat << EOF > patch-persistentvolume.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: before-volume
  labels:
    volume: before-volume
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi
  nfs:
    server: x.x.x.x
    path: /tmp/work/patch/before-volume
EOF
```

```shell
# kubectl apply -f patch-persistentvolume.yaml 
persistentvolume/before-volume created
# kubectl get persistentvolume |grep before
before-volume      1Gi        RWX            Retain           Available                                                            16s
```

- persistentvolumeclaim 作成

```shell:patch-persistentvolumeclaim.yaml
cat << EOF > patch-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: before-volume
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      volume: before-volume
EOF
```


```shell
# kubectl apply -f patch-persistentvolumeclaim.yaml 
persistentvolumeclaim/before-volume created
# kubectl get persistentvolumeclaim |grep before
before-volume   Bound    before-volume   1Gi        RWX                           11s
```

- テスト用 yaml 適用

```shell
# kubectl apply -f patch-volume-test.yaml 
deployment.apps/patch-demo configured
# k exec -it patch-demo-79b9fbb79c-j8nq5 -- cat /tmp/before-volume.txt
update 12:30
```

- 追加用 volume 作成

```shell
# mkdir -p after-volume
# touch after-volume/after-volume.txt
# vi /etc/exports
# exportfs -ra
```

```shell:patch-add-persistentvolume.yaml 
cat << EOF > patch-add-persistentvolume.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: after-volume
  labels:
    volume: after-volume
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi
  nfs:
    server: x.x.x.x
    path: /tmp/work/patch/before-volume
EOF
```

```
# kubectl apply -f patch-add-persistentvolume.yaml 
persistentvolume/after-volume created
```

```shell:patch-add-persistentvolumeclaim.yaml
cat << EOF > patch-add-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: after-volume
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      volume: after-volume
EOF
```

```shell
# kubectl apply -f patch-add-persistentvolumeclaim.yaml 
persistentvolumeclaim/after-volume created
```

- patch 用 yaml 作成

```shell:patch-add-volume.yaml
cat << EOF > patch-add-volume.yaml
spec:
  template:
    spec:
      volumes:
        - name: after-volume-pvc
          persistentVolumeClaim:
            claimName: after-volume
      containers:
        volumeMounts:
          - mountPath: "/opt/"
            name: after-volume-pvc
EOF
```

- patch コマンド実行

```shell
# k patch deployment patch-demo  --patch-file patch-add-volume.yaml
The request is invalid: patch: Invalid value: "map[spec:map[template:map[spec:map[containers:map[volumeMounts:[map[mountPath:/opt/ name:after-volume-pvc]]] volumes:[map[name:after-volume-pvc persistentVolumeClaim:map[claimName:after-volume]]]]]]]": cannot restore slice from map
````

エラーですね...
切り分けとして、更新箇所を volumes のみに変更して実施。

```shell
spec:
  template:
    spec:
      volumes:
        - name: after-volume-pvc
          persistentVolumeClaim:
            claimName: after-volume
```

```shell
# k patch deployment patch-demo  --patch-file patch-add-volume.yaml
deployment.apps/patch-demo patched
```

volumes 追加は問題ありません。
続いて、更新対象を volumeMounts のみに変更して実施します。

```shell
]# cat patch-add-volume.yaml 
spec:
  template:
    spec:
      containers:
        volumeMounts:
          - mountPath: "/opt/"
            name: after-volume-pvc
```

```shell
# k patch deployment patch-demo --patch-file patch-add-volume.yaml
The request is invalid: patch: Invalid value: "map[spec:map[template:map[spec:map[containers:map[volumeMounts:[map[mountPath:/opt/ name:after-volume-pvc]]]]]]]": cannot restore slice from map
```

むむむ、各リソースで patch strategy で異なるのでしょうか？


>        "volumes": {
>          "description": "List of volumes that can be mounted by containers belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes",
>          "items": {
>            "$ref": "#/definitions/io.k8s.api.core.v1.Volume"
>          },
>          "type": "array",
>          "x-kubernetes-patch-merge-key": "name",
>          "x-kubernetes-patch-strategy": "merge,retainKeys"
>        }

>        "volumeMounts": {
>          "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.",
>          "items": {
>            "$ref": "#/definitions/io.k8s.api.core.v1.VolumeMount"
>          },
>          "type": "array",
>          "x-kubernetes-patch-merge-key": "mountPath",
>          "x-kubernetes-patch-strategy": "merge"
>        },

**Cannot be updated** と言っています。これはシンプルに patch コマンド使えないやつなんでしょうか。。。


# おさらい
- openapi や k8s api reference で**更新可否**や**patch strategy**を確認
- 目的と手段が合えば patch 用の yaml を作成&適用

# 終わりに
リソース更新は kubectl patch コマンドだけではなく、kubectl replace 等のコマンドも存在します。その他コマンドとの棲み分けは追って理解したいと思います。

volumeMounts は patch コマンドで更新できないようですので、yaml 修正して Pod を再作成したいと思います。

patch コマンドで volumeMounts を更新する方法を知っているつよつよの方がいましたら教えてください。。。