Tanzu Application PlatformでPVをmountするアプリをデプロイする


Tanzu Application Platform v1.6(以下TAP)ではアプリをtype`web`でデプロイするとknativeでデプロイするが、knativeの場合、デフォルトでPVCが利用できない。
```sh
$ kubectl get cm -o yaml config-features -n knative-serving |grep persistent-volume-claim
    kubernetes.podspec-persistent-volume-claim: "disabled"
```
一方で、type`server`でデプロイするとDeploymentでデプロイするが、こちらもPVをデプロイするオプションはなく、PVCも作成できない。
作成したアプリにPVを割り当てたい場合は不便なので、普通のDeploymentでデプロイし、その際にPVCからPVを作成し、PVが割り当てられるようにする。
今回はPVCを作成する`ClusterConfigTemplate`を作成し、Workloadタイプを新規に作成して、そのタイプで作成した`ClusterConfigTemplate`を利用するようにして実現する。
なお、このメモをまとめるに当たってWarroyo氏の[How to create custom workload types with TAP](https://blog.warroyo.com/how-to-create-custom-workload-types-with-tap)を参考にした。

## Workloadタイプの作成
既存のworkloadタイプではPVを作成するものがないので、PVを作成できるworkloadタイプを作成する。
ここでは将来的に使いまわしやすいよう、yttでoverlayする形でYAMLを作成する。
まず、type`server`で使っている`ClusterConfigTemplate`を取り出した後、不要なフィールドを削除し、yttでoverlay出来るベースのテンプレートをとして利用可能な形に加工した。
取り出す際に叩いたコマンドは以下。
```sh
kubectl get clusterconfigtemplates.carto.run server-template -o yaml | kubectl neat -f - > ./server-template.yaml
```
加工後の`ClusterConfigTemplate`は以下。
```yaml
cat << EOF > ./server-template.yaml
#@ load("@ytt:overlay", "overlay")
#@ load("@ytt:data", "data")
---
kind: ClusterConfigTemplate
metadata:
  annotations:
    doc: #@ data.values.doc
  name: #@ data.values.name
spec:
  configPath: .data
  lifecycle: mutable
  params:
  - default:
    - containerPort: 8080
      name: http
      port: 8080
    name: ports
  ytt: #@ data.values.ytt
EOF
```
また、overlayするvalues.yamlの雛形は以下で作成した。ytt部分は`kubectl get clusterconfigtemplates.carto.run server-template -o jsonpath={.spec.ytt}`の内容をコピペしている。
```yaml
#@data/values
---
name: volumes-template
doc: |
  This template supports PVC.
  You can use this at own risk.
ytt: |
  #@ load("@ytt:data", "data")
  #@ load("@ytt:yaml", "yaml")
  #@ load("@ytt:struct", "struct")
  #@ load("@ytt:assert", "assert")

  #@ def merge_labels(fixed_values):
  #@   labels = {}
  #@   if hasattr(data.values.workload.metadata, "labels"):
  #@     labels.update(data.values.workload.metadata.labels)
  #@   end
  #@   labels.update(fixed_values)
  #@   return labels
  #@ end

  #@ def intOrString(v):
  #@   return v if type(v) == "int" else int(v.strip()) if v.strip().isdigit() else v
  #@ end

  #@ def merge_ports(ports_spec, containers):
  #@   ports = {}
  #@   for c in containers:
  #@     for p in getattr(c, "ports", []):
  #@       ports[p.containerPort] = {"targetPort": p.containerPort, "port": p.containerPort, "name": getattr(p, "name", str(p.containerPort))}
  #@     end
  #@   end
  #@   for p in ports_spec:
  #@     targetPort = getattr(p, "containerPort", p.port)
  #@     type(targetPort) in ("string", "int") or fail("containerPort must be a string or int")
  #@     targetPort = intOrString(targetPort)
  #@
  #@     port = p.port
  #@     type(port) in ("string", "int") or fail("port must be a string or int")
  #@     port = int(port)
  #@     ports[p.port] = {"targetPort": targetPort, "port": port, "name": getattr(p, "name", str(p.port))}
  #@   end
  #@   return ports.values()
  #@ end

  #@ def delivery():
  ---
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: #@ data.values.workload.metadata.name
    annotations:
      kapp.k14s.io/update-strategy: "fallback-on-replace"
      ootb.apps.tanzu.vmware.com/servicebinding-workload: "true"
      kapp.k14s.io/change-rule: "upsert after upserting servicebinding.io/ServiceBindings"
    labels: #@ merge_labels({ "app.kubernetes.io/component": "run", "carto.run/workload-name": data.values.workload.metadata.name })
  spec:
    selector:
      matchLabels: #@ data.values.config.metadata.labels
    template: #@ data.values.config
  ---
  apiVersion: v1
  kind: Service
  metadata:
    name: #@ data.values.workload.metadata.name
    labels: #@ merge_labels({ "app.kubernetes.io/component": "run", "carto.run/workload-name": data.values.workload.metadata.name })
  spec:
    selector: #@ data.values.config.metadata.labels
    ports:
    #@ hasattr(data.values.params, "ports") and len(data.values.params.ports) or assert.fail("one or more ports param must be provided.")
    #@ declared_ports = {}
    #@ if "ports" in data.values.params:
    #@   declared_ports = data.values.params.ports
    #@ else:
    #@   declared_ports = struct.encode([{ "containerPort": 8080, "port": 8080, "name": "http"}])
    #@ end
    #@ for p in merge_ports(declared_ports, data.values.config.spec.containers):
    - #@ p
    #@ end
  #@ end

  ---
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: #@ data.values.workload.metadata.name + "-server"
    labels: #@ merge_labels({ "app.kubernetes.io/component": "config" })
  data:
    delivery.yml: #@ yaml.encode(delivery())
```

これを以下のように変更する。
```diff
--- values.orig.yaml	2023-09-12 08:34:55.507524055 +0000
+++ values.yaml	2023-09-12 11:39:08.929620707 +0000
@@ -1,6 +1,6 @@
 #@data/values
 ---
-name: server-template
+name: volumes-template
 doc: |
   This template supports PVC.
   You can use this at own risk.
@@ -9,7 +9,19 @@
   #@ load("@ytt:yaml", "yaml")
   #@ load("@ytt:struct", "struct")
   #@ load("@ytt:assert", "assert")
-
+  #@ load("@ytt:overlay", "overlay")
+
+  #@ def addVolumes():
+  spec:
+    containers:
+    #@overlay/match by="name"
+    - name: workload
+      #@overlay/match missing_ok=True
+      volumeMounts: #@ data.values.params.volumes.volumeMounts
+    #@overlay/match missing_ok=True
+    volumes: #@ data.values.params.volumes.volumes
+  #@ end
+
   #@ def merge_labels(fixed_values):
   #@   labels = {}
   #@   if hasattr(data.values.workload.metadata, "labels"):
@@ -57,7 +69,7 @@
   spec:
     selector:
       matchLabels: #@ data.values.config.metadata.labels
-    template: #@ data.values.config
+    template: #@ overlay.apply(data.values.config,addVolumes())
   ---
   apiVersion: v1
   kind: Service
@@ -77,13 +89,26 @@
     #@ for p in merge_ports(declared_ports, data.values.config.spec.containers):
     - #@ p
     #@ end
+
+  ---
+  apiVersion: v1
+  kind: PersistentVolumeClaim
+  metadata:
+    name: #@ data.values.workload.metadata.name + "-volumes"
+    labels: #@ merge_labels({ "app.kubernetes.io/component": "run", "carto.run/workload-name": data.values.workload.metadata.name })
+  spec:
+    accessModes:
+    - ReadWriteOnce
+    resources:
+      requests:
+        storage: #@ data.values.params.pvSize
+    storageClassName: default
   #@ end
-
   ---
   apiVersion: v1
   kind: ConfigMap
   metadata:
-    name: #@ data.values.workload.metadata.name + "-server"
+    name: #@ data.values.workload.metadata.name + "-volumes"
     labels: #@ merge_labels({ "app.kubernetes.io/component": "config" })
   data:
     delivery.yml: #@ yaml.encode(delivery())
```
編集内容としては、`#@ def addVolumes():`〜`#@ end`で関数を定義し、PVのmount処理を定義している。
また、`#@ overlay.apply(data.values.config,addVolumes())`で関数呼び出しによってDeploymentの方に処理を追加している。
PVCに関しては`  #@ def delivery():`の最後に新規に作成するようにしている。
名前が`#@ data.values.workload.metadata.name + "-volumes"`としてしまっているため、Workloadで`addVolumes`の中身を定義する際は、`workload名-volumes`にする必要がある点に注意。
また、  `#@ def delivery():`の中に入れないと反映されない点も注意。
これを新しい`ClusterConfigTemplate`としてapplyする。
```sh
 ytt -f server-template.yaml -f values.yaml  | kubectl apply -f -
```

作成した`ClusterConfigTemplate`をtypeとして指定できるよう、typeを定義する。
typeの定義はTAPのインストール時に作成したtap-values.yamlに追記する形で行う。
なお、`supported_workloads`を定義するとデフォルト値に上書きされる形で定義されるため、デフォルトの`web`や`server`などが消えてしまう。そのため、デフォルト値に追記する形で定義する。
```diff
--- tap-values.yaml	2023-08-30 01:15:30.272824163 +0000
+++ tap-values-pvc.yaml	2023-09-12 08:36:32.691925655 +0000
@@ -48,6 +48,10 @@
 profile: full

 supply_chain: testing_scanning
+ootb_supply_chain_testing_scanning:
+  supported_workloads:
+  - type: web
+    cluster_config_template_name: config-template
+  - type: server
+    cluster_config_template_name: server-template
+  - type: worker
+    cluster_config_template_name: worker-template
+  - type: server-with-volumes
+    cluster_config_template_name: volumes-template

 contour:
   envoy:
```
設定をTAPに反映させる。
```sh
tanzu package installed update tap --values-file tap-values-pvc.yaml -n tap-install
```

また、PVCの作成権限がなく、このままWorkloadを作成すると、以下のようなエラーになってしまう。
```sh
    kapp: Error: Checking existence of resource persistentvolumeclaim/tanzu-java-web-app-hoge-volumes (v1) namespace: tap-demo:
      API server says: persistentvolumeclaims "tanzu-java-web-app-hoge-volumes" is forbidden:
        User "system:serviceaccount:tap-demo:default" cannot get resource "persistentvolumeclaims" in API group "" in the namespace "tap-demo" (reason: Forbidden)
```
そのため、TAPが使うアカウントの権限も強化しておく。
```
kubectl create rolebinding pvc-for-test --clusterrole edit --user="system:serviceaccount:tap-demo:default" -n tap-demo
```

## 検証
Workloadをデプロイする。
[前回](https://qiita.com/ipppppei/items/96861ab398114f393367)デプロイした、Tanzu Java Web Appを再利用し、`config/workload.yaml`のparamsを以下のように編集する。
```yaml
  params:
    - name: pvSize
      value: 2G
    - name: volumes
      value:
        volumes:
        - name: data-mount
          persistentVolumeClaim:
            claimName: tanzu-java-web-app-pvc-volumes
        volumeMounts:
         - name: data-mount
           mountPath: /data
```
これにより、先程作成した`addVolumes`やPVCに値を渡す。
Workloadをデプロイする。前回と違って今回作成したtypeを`--type server-with-volumes`として指定する。
```sh
tanzu apps workload create tanzu-java-web-app-pvc -f ./config/workload.yaml --type server-with-volumes --label app.kubernetes.io/part-of=tanzu-java-web-app-pvc --label apps.tanzu.vmware.com/has-tests="true" --yes --namespace tap-demo
```
指定したPVCが作成されている事がわかる。
```sh
$ kubectl get pvc tanzu-java-web-app-hoge-volumes -n tap-demo
NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
tanzu-java-web-app-hoge-volumes   Bound    pvc-17467f81-4c2a-4714-9b1a-7a877b1d9ca7   1908Mi     RWO            default        2m35s
```
Podも無事起動した。
```sh
$ kubectl get pod tanzu-java-web-app-pvc-575fcdd7f8-r8qsl -n tap-demo
NAME                                      READY   STATUS    RESTARTS   AGE
tanzu-java-web-app-pvc-575fcdd7f8-r8qsl   1/1     Running   0          41s
```
describeで見ると、mountも問題なさそうだ。
```sh
    Mounts:
      /data from data-mount (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-xhlsv (ro)
```
なお、今回type`server`にあっていないWorkloadを使ったため、Podの設定とかは適切なものになっていない。（Readiness probeでエラーが出たりする）
あくまでもお試しという点でご了承を。