Class BasedのTKGのノードの中身をカスタマイズする

これまでのTanzu Kubernetes Grid（TKG）でノードの中身を設定する場合（例えば証明書を入れるとか）、設定ファイルにパラメータがない場合はytt overlayでカスタマイズしてきた。
しかし、Class BasedのTKGでは従来のytt overlayが利用できなくなり、ClusterClassをカスタマイズして修正する必要がある。
この修正が非常に癖があるので、今回は試しにスタティックルートの設定をWorkload ClusterのWorkerノードに追加して、どういう手順が必要になるを検証する。
なお、公式ドキュメントの[こちら](https://docs.vmware.com/jp/VMware-Tanzu-Kubernetes-Grid/2.1/using-tkg-21/workload-clusters-cclass.html)を参考にした。

スタティックルートの設定をWorkerノードに追加するには、`kind: VSphereMachineTemplate`の`devices`にroutesを足して実現できる。
```diff
      network:
        devices:
        - dhcp4: true
          networkName: /vSAN-DC/network/VM Network
+         routes:
+         - metric: 1
+           to: 192.168.111.200
+           via: 192.168.111.1
```
これをClusterClassをカスタマイズして実現する。

## 事前準備
### Management Clusterの作成
今回はManagement Clusterを作成した状態から始める。作成手順は省略する。

### Carvelのインストール
`${HOME}/bin/`以下にインストールする。（パスは通っているものとしている）
```sh
cd ~
mkdir ~/bin/
curl -L https://carvel.dev/install.sh | K14SIO_INSTALL_BIN_DIR=bin bash
vendir version
```

### ベースとなるManifestの入手

BaseとなるManifestをDownloadして解凍し、ディレクトリをコピーする。
```sh
wget https://github.com/vmware-tanzu/tanzu-framework/archive/refs/tags/v0.28.1.zip
unzip v0.28.1.zip
cp -r tanzu-framework-0.28.1/packages/tkg-clusterclass-vsphere/bundle/ .
```
中身は以下のようになっている。
```
bundle/
└── config
    ├── upstream
    │   ├── base.yaml
    │   ├── overlay-kube-apiserver-admission.yaml
    │   └── overlay.yaml
    └── values.yaml
```

## 設定作業
### ベースのManifestに既存設定をマージ
Management Clusterにコンテキストをあわせて、以下を実行する。
```yaml
cat <<EOF > default_values.yaml
#@data/values

#@overlay/match-child-defaults missing_ok=True
---
EOF
kubectl get secret tkg-clusterclass-infra-values -o jsonpath='{.data.values\.yaml}' -n tkg-system | base64 -d >> default_values.yaml
```
`tkg-clusterclass-infra-values`の中にはManagement Cluster構築時に使ったvalues.yaml(plan based)が入っているので、それが追記されることになる。

抜き出した、ベースとなるManifestと結合する。
```sh
ytt -f bundle/ -f default_values.yaml > default_cc.yaml
```
中身はClusterClassを含むクラスタ関連のリソースが定義されている。
```
$ grep "^kind" default_cc.yaml
kind: VSphereClusterTemplate
kind: VSphereMachineTemplate
kind: VSphereMachineTemplate
kind: KubeadmControlPlaneTemplate
kind: KubeadmConfigTemplate
kind: ClusterClass
```
なお、ここでは細かく紹介しないが、default_cc.yaml内の`ClusterClass`でカスタマイズするための変数等が設定されている。
今回ytt overlayを作成するにあたり非常に参考になったので、実際にカスタマイズする際はこれをよく読んでおくとよい。

### カスタムするoverlayの作成

最初にoverlayをまとめるディレクトリを作成する。
```
mkdir custom
```

`kind: ClusterClass`内の`kind: VSphereMachineTemplate`を修正するoverlayを作成する。

```yaml
cat << EOF > ./custom/overlay_staticRoute.yaml
#@ load("@ytt:overlay", "overlay")
#@ load("@ytt:data", "data")

#@overlay/match by=overlay.subset({"kind":"ClusterClass"})
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
spec:
  variables:
  - name: staticRoute
    required: false
    schema:
      openAPIV3Schema:
        type: object
        properties:
          metric:
            type: integer
          to:
            type: string
          via:
            type: string
  patches:
  - name: staticRoute
    enabledIf: '{{ not (empty .staticRoute) }}'
    definitions:
      - selector:
          apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
          kind: VSphereMachineTemplate
          matchResources:
            machineDeploymentClass:
              names:
              - tkg-worker
        jsonPatches:
          - op: add
            path: /spec/template/spec/network
            valueFrom:
              template: |
                devices:
                - dhcp4: true
                  networkName: {{ .vcenter.network }}
                  routes:
                  - metric: {{ .staticRoute.metric }}
                    to: {{ .staticRoute.to }}
                    via: {{ .staticRoute.via }}
EOF
```
書いている意味は以下となる。
```yaml
  variables:
  - name: staticRoute
    required: false
    schema:
      openAPIV3Schema:
        type: object
        properties:
          metric:
            type: integer
          to:
            type: string
          via:
            type: string
```
`variables:`の中で変数を定義することが出来る。CRDではよく見るopenAPIV3Schemaの形式で記載する。
ここでは`staticRoute`というオブジェクトを定義し、中に`metric`、`to`、`via`という変数的なものを定義した。
ベタ書きでいい場合は変数を作らなくても良さそう。

```yaml
  patches:
  - name: staticRoute
    enabledIf: '{{ not (empty .staticRoute) }}'
```
中のyamlを修正する場合、`patches:`で修正箇所と修正内容を定義する。
`enabledIf`でstaticRouteが指定された場合のみファイルを修正するようにしている。

```yaml
    definitions:
      - selector:
          apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
          kind: VSphereMachineTemplate
          matchResources:
            machineDeploymentClass:
              names:
              - tkg-worker
```
書き換え場所を指定する。`MachineDeployment`に使う`VSphereMachineTemplate`を書き換える。
`matchResources`も必須で、書き換え対象を指定する。ControlPlaneに関するものだったら`controlPlane: true`、Workerに関するものだったら`machineDeploymentClass`で対象MachineDeploymentを指定する。クラスタに関するものは`infrastructureCluster: true`を指定する。
```yaml
        jsonPatches:
          - op: add
            path: /spec/template/spec/network
            valueFrom:
              template: |
                devices:
                - dhcp4: true
                  networkName: {{ .vcenter.network }}
                  routes:
                  - metric: {{ .staticRoute.metric }}
                    to: {{ .staticRoute.to }}
                    via: {{ .staticRoute.via }}
```
上では実際のパラメータの設定を行っている。json patch形式で書くようで、pathが`VSphereMachineTemplate`リソースの書き換え対象（`spec.template.spec.network`）でopでjson patchでのopを指定する。
作成した変数については`{{ .<変数名> }}`で参照できる。
`.vcenter.network `は作成していないが別で定義されているのでそちらを引っ張ってきている。


また、defaultのClusterClassは変更できないため、別名をつけるためのytt overlayを作成する。なお公式ドキュメントでは`CC-VERSION`となってるが、`VSPHERE_CLUSTER_CLASS_VERSION`が正しいと思われる。
```yaml
cat << EOF > ./custom/values.yaml
#@data/values

#@overlay/match-child-defaults missing_ok=True
---
#! Change the suffix so we know from which default ClusterClass it is extended
VSPHERE_CLUSTER_CLASS_VERSION: extended-v1.0.0

#! Add other variables below if necessary
EOF
```
カスタムの`ClusterClass`を作成するのに必要な最終的なファイル構成は以下となった。
```
├── bundle
│   └── config
│       ├── upstream
│       │   ├── base.yaml
│       │   ├── overlay-kube-apiserver-admission.yaml
│       │   └── overlay.yaml
│       └── values.yaml
├── custom
│   ├── overlay_staticRoute.yaml
│   └── values.yaml
└── default_values.yaml
```

カスタムのClusterClassを作成する。
```sh
ytt -f bundle/ -f default_values.yaml -f custom/ > custom_cc.yaml
```
diffを取って、名前と追加した項目以外変わっていないことを確認する。
```sh
diff default_cc.yaml custom_cc.yaml
```
applyする。
```sh
kubectl apply -f ./custom_cc.yaml
```
追加されているのを確認する。
```sh
$ kubectl get clusterclass
NAME                                  AGE
tkg-vsphere-default-extended-v1.0.0   23h
tkg-vsphere-default-v1.0.0            27h
```

## Workloadクラスタの作成

Workload Clusterのテンプレートは既にあるものとする（ここでは~/.config/tanzu/tkg/clusterconfigs/workload.yamlとする）。
既存のものを以下の点について修正する。
- ClusterClassの指定をデフォルトのものから作成したものに変更
- 作成したパラメータを使ってスタティックルートを指定

スタティックルートは192.168.201.10宛を192.168.201.1経由にする設定で入れる。
修正箇所は以下になる。
```diff
   topology:
-    class: tkg-vsphere-default-v1.0.0
+    class: tkg-vsphere-default-extended-v1.0.0
     controlPlane:
       metadata:
         annotations:
           run.tanzu.vmware.com/resolve-os-image: image-type=ova,os-name=ubuntu
       replicas: 1
     variables:
+    - name: staticRoute
+      value:
+        metric: 1
+        to: 192.168.201.10
+        via: 192.168.201.1
     - name: controlPlaneCertificateRotation
       value:
         activate: true
```
作成の前にdry-runが実行できる。dry-runを飛ばすことも可能だが、経験上凄く失敗するので、dry-runを試してからクラスタ作成に移った方がよい。
なお、clusterctlコマンドの古いバージョン(v1.2.0)が必要となる。（新しいバージョンだとエラーになる）
こちらにリンクを貼っておく。
https://github.com/kubernetes-sigs/cluster-api/releases/tag/v1.2.0

クラスタ作成時に使うWorkload Clusterのテンプレートをコピーする。
```
cp ~/.config/tanzu/tkg/clusterconfigs/workload.yaml ./dryrun.yaml
```
Management Clusterの`kind: Cluster`で持っている`TKR_DATA`というデータを表示する。
```
kubectl get cluster <Management Cluster名> -n tkg-system -o jsonpath='{.spec.topology.variables}' | jq -r '.[] | select(.name == "TKR_DATA")' | yq -p json '.'
```
表示された`TKR_DATA`をdryrun.yamlの`.spec.topology.variables`に挿入する。
```diff
@@ -69,6 +69,33 @@
           run.tanzu.vmware.com/resolve-os-image: image-type=ova,os-name=ubuntu
       replicas: 1
     variables:
+    - name: TKR_DATA
+      value:
+        v1.24.9+vmware.1:
+          kubernetesSpec:
+            coredns:
+              imageTag: v1.8.6_vmware.15
+            etcd:
+              imageTag: v3.5.6_vmware.3
+            imageRepository: projects.registry.vmware.com/tkg
+            kube-vip:
+              imageTag: v0.5.7_vmware.1
+            pause:
+              imageTag: "3.7"
+            version: v1.24.9+vmware.1
+          labels:
+            image-type: ova
+            os-arch: amd64
+            os-name: ubuntu
+            os-type: linux
+            os-version: "2004"
+            ova-version: v1.24.9---vmware.1-tkg.1-b030088fe71fea7ff1ecb87a4d425c93
+            run.tanzu.vmware.com/os-image: v1.24.9---vmware.1-tkg.1-b030088fe71fea7ff1ecb87a4d425c93
+            run.tanzu.vmware.com/tkr: v1.24.9---vmware.1-tkg.1
+          osImageRef:
+            moid: vm-51
+            template: /vSAN-DC/vm/ubuntu-2004-efi-kube-v1.24.9+vmware.1
+            version: v1.24.9+vmware.1-tkg.1-b030088fe71fea7ff1ecb87a4d425c93
```
dry-runの結果を保存するディレクトリを作成する。
```
mkdir plan
```
clusterctlを使ってdry-runを実行する。
```
./clusterctl alpha topology plan -f dryrun.yaml -o plan
```
実行して問題なければ今のManagement Clusterが持つリソースの差分が出力される。コンソール出力としては以下のようになり、planディレクトリ以下に作成される予定のManifestが格納される。
```
Detected a cluster with Cluster API installed. Will use it to fetch missing objects.
No ClusterClasses will be affected by the changes.
The following Clusters will be affected by the changes:
 ＊ default/delme-wc-edit

Changes for Cluster "default/delme-wc-edit":

  NAMESPACE  KIND                    NAME                                ACTION
  default    KubeadmConfigTemplate   delme-wc-edit-md-0-bootstrap-6cqsk  created
  default    KubeadmControlPlane     delme-wc-edit-jffv2                 created
  default    MachineDeployment       delme-wc-edit-md-0-jcbmj            created
  default    MachineHealthCheck      delme-wc-edit-jffv2                 created
  default    MachineHealthCheck      delme-wc-edit-md-0-jcbmj            created
  default    Secret                  delme-wc-edit-shim                  created
  default    VSphereCluster          delme-wc-edit-hj967                 created
  default    VSphereMachineTemplate  delme-wc-edit-control-plane-4bw9p   created
  default    VSphereMachineTemplate  delme-wc-edit-md-0-infra-755r7      created
  default    Cluster                 delme-wc-edit                       modified

Created objects are written to directory "plan/created"
Modified objects are written to directory "plan/modified"
```
なお、現在のManagement Clusterが持つリソースとの差分なので、クラスタ作成後は何も表示されなくなるので注意。
出力された中身を見て、意図したようにVSphereMachineTemplateが書き換わっていることを確認する。
```yaml
      network:
        devices:
        - dhcp4: true
          networkName: /vSAN-DC/network/VM Network
          routes:
          - metric: 1
            to: 192.168.201.10
            via: 192.168.201.1
```
問題なさそうであれば、デプロイする。
```
tanzu cluster create -f workload.yaml -v9
```
問題なくデプロイ出来たら、動作確認する。NodeのIPに対してsshしてip routeで確認する。
```sh
$ ssh -l capv  10.151.206.93 ip route
default via 10.151.207.254 dev eth0 proto dhcp src 10.151.206.93 metric 100
10.151.192.0/20 dev eth0 proto kernel scope link src 10.151.206.93
10.151.207.254 dev eth0 proto dhcp scope link src 10.151.206.93 metric 100
100.96.0.0/24 via 100.96.0.1 dev antrea-gw0 onlink
100.96.1.0/24 dev antrea-gw0 proto kernel scope link src 100.96.1.1
192.168.201.10 via 192.168.201.1 dev eth0 proto static metric 1 onlink
```

追加した192.168.201.10宛の設定が追加されていることが分かる。

## おまけ：失敗例
後方に追加したいので、`/-`を指定すればいい、と思って以下を書いた。
```yaml
          - op: add
            path: /spec/template/spec/network/devices/-
```
結果は以下。
```yaml
      network:
        devices:
        - dhcp4: true
          networkName: /vSAN-DC/network/VM Network
        - routes:
          - metric: 1
            to: 192.168.201.10
            via: 192.168.201.1
```
`routes`をpathに含めたパターン。
```yaml
          - op: add
            path: /spec/template/spec/network/devices/routes
```
こちらはclusterctlが以下のエラーを出力した。
```
Detected a cluster with Cluster API installed. Will use it to fetch missing objects.
Error: failed to dry run the topology controller: error computing the desired state of the Cluster topology: failed to apply patches: failed to apply patch with uid "61c28a73-17ac-4425-b8c6-5f52cad035a7": error applying json patch (RFC6902): [{"op":"add","path":"/spec/template/spec/network/devices/routes","value":{"metric":1,"to":"192.168.201.10","via":"192.168.201.1"}}]: error in add for path: '/spec/template/spec/network/devices/routes': value was not a proper array index: 'routes': strconv.Atoi: parsing "routes": invalid syntax
```
あと、clusterctlが最新版だと以下のエラーが出る
```
Detected a cluster with Cluster API installed. Will use it to fetch missing objects.
Error: failed defaulting and validation on input objects: failed to run defaulting and validation on Clusters: failed defaulting of cluster.x-k8s.io/v1beta1, Kind=Cluster default/delme-wc-edit: Cluster.cluster.x-k8s.io "delme-wc-edit" is invalid: [spec.topology.variables: Invalid value: "": failed validation: "TKR_DATA.v1.24.9+vmware.1" fields are not specified in the variable schema of variable "TKR_DATA", spec.topology.variables: Invalid value: "": failed validation: "worker.count" fields are not specified in the variable schema of variable "worker"]
```

結論：

- json patchは慣れが必要
- clusterctlでの事前確認は大事
- 早くclusterctlの最新版に対応して欲しい