TKG2.1での証明書の埋め込み方＆アップグレード後の挙動を検証する

Tanzu Kubernetes GridでHarborをオレオレ証明書で構築後、このHarborの証明書をノードに入れないとイメージのpullが出来ない。
例えばnginxを使って起動しようとすると、以下のようなエラーになる。
```
  Normal   Pulling    7s (x2 over 19s)  kubelet            Pulling image "harbor.10-220-14-115.sslip.io/library/nginx"
  Warning  Failed     7s (x2 over 19s)  kubelet            Failed to pull image "harbor.10-220-14-115.sslip.io/library/nginx": rpc error: code = Unknown desc = failed to pull and unpack image "harbor.10-220-14-115.sslip.io/library/nginx:latest": failed to resolve reference "harbor.10-220-14-115.sslip.io/library/nginx:latest": failed to do request: Head "https://harbor.10-220-14-115.sslip.io/v2/library/nginx/manifests/latest": x509: certificate signed by unknown authority
```

これを回避するために、ノードにHarborの証明書を埋め込むのだが、TKG2.1ではplan-basedとclass-basedという2パターンのクラスタタイプが用意されており、それぞれで証明書のインストール方法が異なる。またアップデート後の挙動も異なるため、それを検証した時のメモとなる。


## 前準備
テスト用のイメージをPrivate Harborに入れておく。docker loginするためにPrivate HarborのSecretの`harbor-tls`をdocker daemonに読み込ませる。
```sh
export HARBOR_DOMAIN="harbor.10-151-200-240.sslip.io"
sudo mkdir -p /etc/docker/certs.d/$HARBOR_DOMAIN
kubectl get secret -n tanzu-system-registry -o yaml harbor-tls -o jsonpath="{.data.ca\.crt}" | base64 -d | sudo tee /etc/docker/certs.d/$HARBOR_DOMAIN/ca.crt
sudo systemctl restart docker
```
Private Harborにdocker loginし、テストイメージとしてnginxをpushする。
```sh
export HARBOR_ID=admin
export HARBOR_PW=xxxx
echo $HARBOR_PW | docker login $HARBOR_DOMAIN -u $HARBOR_ID --password-stdin
docker pull nginx
docker tag nginx ${HARBOR_DOMAIN}/library/nginx
docker push ${HARBOR_DOMAIN}/library/nginx
```

## 新規・既存クラスタへの証明書の埋め込み
### 既存クラスタへの埋め込み
Private Harborをデプロイしたクラスタ自身からHarborを参照したい場合など、既存のクラスタでPrivate Harborを利用したい場合などはこちらの手順を利用する。
オフィシャルドキュメントの手順は[こちら](https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/2.1/using-tkg-21/workload-clusters-secret.html#make-a-planbased-cluster-trust-a-custom-registry-6)になる。
なお、ドキュメントだと何故か`KubeadmControlPlane`リソースへの埋め込みがスキップされている。`KubeadmControlPlane`に入れないとControlPlaneに反映されない。
ただ、ControlPlaneでPrivate Harborからpullするケースは今回想定していないため、ドキュメントに合わせてWorkerノードにのみ証明書を導入する。

最初にHarborの証明書を取得しデコードしたものを控えておく。
```sh
kubectl get secret -n tanzu-system-registry -o yaml harbor-tls -o jsonpath="{.data.ca\.crt}" | base64 -d
```
Management Clusterにコンテキストを切り替える。
```sh
kubectl config use-context tkg21mc
```
最初にWorkerノードを管理する`kubeadmconfigtemplate`リソースを書き換える。
オブジェクト名は`ClusterClass`リソースの名前が付いているものを指定する。
```sh
kubectl edit kubeadmconfigtemplate tkg-vsphere-default-v1.0.0-md-config
```
`kubeadmconfigtemplate`リソースは`spec.template.spec.files`に定義したファイルをノード作成時に生成してくれる。
既存の`files`に追記する形で以下を追記する。
```yaml
      - content: |
          -----BEGIN CERTIFICATE-----
          MIIDKDCCAhCgAwIBAgIQHmZgcfI/GYpB0qIz0BwmRTANBgkqhkiG9w0BAQsFADAU
　　　　　　：（省略）
          XcSmQr62lueUki8kqLcIDGnI2p3j+YmzO+fZxKjmIn+XySPy8TGcOwz9tjs=
          -----END CERTIFICATE-----
        owner: root:root
        path: /etc/ssl/certs/tkg-custom-ca.pem
        permissions: "0644"
```
また、`spec.template.spec.preKubeadmCommands`に以下を追加する。
```yaml
- '! which rehash_ca_certificates.sh 2>/dev/null || rehash_ca_certificates.sh'
- '! which update-ca-certificates 2>/dev/null || (mv /etc/ssl/certs/tkg-custom-ca.pem
   /usr/local/share/ca-certificates/tkg-custom-ca.crt && update-ca-certificates)'
```
ノードデプロイ時、中でkubadmによるノードのクラスタ追加が実施されるが、`preKubeadmCommands`はその前に実行するコマンドを指定するオプションであり、ここでは証明書のインストールコマンドを叩いている。。 PhotonOSとUbuntuでパスが異なるため、両方対応出来るよう2種類のインストールコマンドを記載しており、ubuntuでは`update-ca-certificates`を実行して上記で作成した証明書を読み込ませている。
元々ある設定値を含めると、`preKubeadmCommands`は以下のようになった。
```yaml
      preKubeadmCommands:
      - hostname "{{ ds.meta_data.hostname }}"
      - echo "::1         ipv6-localhost ipv6-loopback" >/etc/hosts
      - echo "127.0.0.1   localhost" >>/etc/hosts
      - echo "127.0.0.1   {{ ds.meta_data.hostname }}" >>/etc/hosts
      - echo "{{ ds.meta_data.hostname }}" >/etc/hostname
      - '! which rehash_ca_certificates.sh 2>/dev/null || rehash_ca_certificates.sh'
      - '! which update-ca-certificates 2>/dev/null || (mv /etc/ssl/certs/tkg-custom-ca.pem
         /usr/local/share/ca-certificates/tkg-custom-ca.crt && update-ca-certificates)'
```

次にWorkerノードを再作成する。`MachineDeployment`リソースはannotationに変更を加えるとWorkerノードを再作成する。annotationに以下のpatchコマンドで変更を加えてノードに証明書が入った状態にする。
```sh
kubectl patch machinedeployments  tkg21wc-md-0-8jpw5 --type merge -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"date\":\"`date +'%s'`\"}}}}}"
```

しばらくするとWorkerノードのみ再作成される。
再作成された後、動作確認する。
pullする際の認証情報をSecretとして作成し、defaultのServiceAccountに紐付ける。ここでは`kubectl patch`で紐付けているが、既存のimagePullSecret設定があるなら`kubectl edit`で代用すること。
```sh
kubectl create secret docker-registry --docker-server=$HARBOR_DOMAIN --docker-username=$HARBOR_ID --docker-password=$HARBOR_PW myharbor-cred
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "myharbor-cred"}]}'
```
テスト用のイメージを起動する。
```sh
kubectl run --image ${HARBOR_DOMAIN}/library/nginx nginx
```
無事起動すればOK。
```sh
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          43s
```

## class-basedの新規クラスタへの埋め込み
実は特に作業はない。
従来だと、[既存のクラスタへのカスタム CA 証明書の信頼の追加（スタンドアローン MC）](https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/2.1/using-tkg-21/workload-clusters-secret.html#make-a-planbased-cluster-trust-a-custom-registry-6)の手順にて追加するが、後述の余談の節に記載しているようにclass-basedだと自動で`tkg-vsphere-default-v1.0.0-md-config`をベースとした`kubeadmconfigtemplates`が作成されるので対応不要。

試しに新規にクラスタを作成する。
```
tanzu cluster create -f tkg21wc-2.yaml -v9
```
構築後、コンテキストを切り換えてPrivate Harborのnginxを起動する。
```
tanzu cluster kubeconfig get --admin tkg21wc-2
kubectl config use-context tkg21wc-2-admin@tkg21wc-2
kubectl run --image $HARBOR_DOMAIN/library/nginx nginx
```
無事起動している。
```
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          31s
```

## plan-basedの新規クラスタへの埋め込み
plan-basedの場合は`kubeadmconfigtemplates`の元となるものなく新規に作成される。新規に作成される`kubeadmconfigtemplates`をカスタマイズしたい場合、リソースに修正をかけるytt overlayを`~/.config/tanzu/tkg/providers/ytt/03_customizations`以下に設置しておく必要がある。
[公式ドキュメント](https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/2.1/using-tkg-21/workload-clusters-secret.html#trust-custom-ca-certificates-in-new-clusters-3)をベースにytt overlayを作成する。

最初にコンテキストをWorkload Clusterに切り替えておく。
```sh
kubectl config use-context tkg21wc
```
ytt overlayファイルを作成する。
```yaml
export YTT_DIR=~/.config/tanzu/tkg/providers/ytt/03_customizations
kubectl get secret -n tanzu-system-registry -o yaml harbor-tls -o jsonpath="{.data.ca\.crt}" | base64 -d > ${YTT_DIR}/tkg-custom-ca.pem
cat << 'EOF' > ${YTT_DIR}/add-custom-ca.yaml
#@ load("@ytt:overlay", "overlay")
#@ load("@ytt:data", "data")

#@overlay/match by=overlay.subset({"kind":"KubeadmConfigTemplate"}), expects="1+"
---
spec:
  template:
    spec:
      #@overlay/match missing_ok=True
      files:
        #@overlay/append
        - content: #@ data.read("tkg-custom-ca.pem")
          owner: root:root
          permissions: "0644"
          path: /etc/ssl/certs/tkg-custom-ca.pem
      #@overlay/match missing_ok=True
      preKubeadmCommands:
        #! For Photon OS
        #@overlay/append
        - '! which rehash_ca_certificates.sh 2>/dev/null || rehash_ca_certificates.sh'
        #! For Ubuntu
        #@overlay/append
        - '! which update-ca-certificates 2>/dev/null || (mv /etc/ssl/certs/tkg-custom-ca.pem /usr/local/share/ca-certificates/tkg-custom-ca.crt && update-ca-certificates)'
EOF
```
plan-basedの設定ファイルを利用してWorkloadクラスタを作成する
```sh
tanzu config set features.cluster.allow-legacy-cluster true
tanzu cluster create -f tkg21wc-addcert-planbased.yaml -v9
```
コンテキストを切り替えてPrivate Harborのイメージを起動する。
```sh
tanzu cluster kubeconfig get --admin tkg21wc-addcert
kubectl config use-context tkg21wc-addcert-admin@tkg21wc-addcert
kubectl run --image $HARBOR_DOMAIN/library/nginx nginx
```
こちらでも問題なくpullできることが分かる。
```
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          61s
```

## TKGのバージョンアップ後の証明書
今回、バージョンアップの検証もしたかったので、TKG2.1.0でManagement Clusterを構築した。これを2.1.1にバージョンアップして、証明書が再度取り込まれるかを確認する。

Management ClusterをTKG2.1.1にアップグレードする。公式ドキュメントは[こちら](https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/2.1/tkg-deploy-mc-21/mgmt-upgrade-mc.html)。
tanzu cliをv0.28.0からv0.28.1に変更する。
```
$ tanzu version
version: v0.28.1
buildDate: 2023-03-07
sha: 0e6704777-dirty
```
v0.28.1用のプラグインをインストールする。
```
tanzu init
```
Kubernetesバージョンv1.24.10+vmware.1のOVAを転送してテンプレート化し、upgradeを実行する。
```
tanzu mc upgrade
```

アップデート後、ClusterClassに紐付いた`kubeadmconfigtemplates`リソースの`tkg-vsphere-default-v1.0.0-md-config`が変更されたままかを確認する。
```yaml
$ kubectl get kubeadmconfigtemplates -o yaml tkg-vsphere-default-v1.0.0-md-config
：（省略）
      preKubeadmCommands:
      - hostname "{{ ds.meta_data.hostname }}"
      - echo "::1         ipv6-localhost ipv6-loopback" >/etc/hosts
      - echo "127.0.0.1   localhost" >>/etc/hosts
      - echo "127.0.0.1   {{ ds.meta_data.hostname }}" >>/etc/hosts
      - echo "{{ ds.meta_data.hostname }}" >/etc/hostname
      useExperimentalRetryJoin: true
：（省略）
```
`files`に書いた証明書も`preKubeadmCommands`に書いた証明書のインストールコマンドも消えている。
これにより、class-basedだとTKGのバージョンアップ後は追記した証明書情報はリセットされることが分かる。

次に、class-basedとplan-basedのクラスタをそれぞれupgradeしてみる。
```
tanzu cluster upgrade tkg21wc
```
```
tanzu cluster upgrade tkg21wc-addcert
```
それぞれ`kubeadmconfigtemplates`リソースは作成され直すが、plan-basedで作成していたクラスタはTKG2.1.0と同じytt overlayを利用してデプロイされるため、証明書や証明書のインストールコマンドは入っている。
```yaml
      preKubeadmCommands:
      - hostname "{{ ds.meta_data.hostname }}"
      - echo "::1         ipv6-localhost ipv6-loopback" >/etc/hosts
      - echo "127.0.0.1   localhost" >>/etc/hosts
      - echo "127.0.0.1   {{ ds.meta_data.hostname }}" >>/etc/hosts
      - echo "{{ ds.meta_data.hostname }}" >/etc/hostname
      - '! which rehash_ca_certificates.sh 2>/dev/null || rehash_ca_certificates.sh'
      - '! which update-ca-certificates 2>/dev/null || (mv /etc/ssl/certs/tkg-custom-ca.pem
        /usr/local/share/ca-certificates/tkg-custom-ca.crt && update-ca-certificates)'
      useExperimentalRetryJoin: true
```
一方でplan-basedで作成していたクラスタは更新後の`tkg-vsphere-default-v1.0.0-md-config`を元にした`kubeadmconfigtemplates`で再作成されるため、証明書や証明書のインストールコマンドは消える。
```yaml
      preKubeadmCommands:
      - hostname "{{ ds.meta_data.hostname }}"
      - echo "::1         ipv6-localhost ipv6-loopback" >/etc/hosts
      - echo "127.0.0.1   localhost" >>/etc/hosts
      - echo "127.0.0.1   {{ ds.meta_data.hostname }}" >>/etc/hosts
      - echo "{{ ds.meta_data.hostname }}" >/etc/hostname
      useExperimentalRetryJoin: true
```
更新後の環境でサンプルのnginxを起動した結果はこちら。
■plan-based
```
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          4s
```
■class-based
```
$ kubectl get pod
NAME    READY   STATUS         RESTARTS   AGE
nginx   0/1     ErrImagePull   0          2s
```


## 結論
class-basedでインストールした環境だと、Private Harborの証明書の適用はクラスタ内のリソースを更新するだけでよく、ytt overlayのファイルを用意せずに他のクラスタに伝搬できるため、その点は便利である。
一方で、TKGのバージョンアップを行うと、設定が消えてしまう。
現時点ではManagement Clusterの更新のタイミングで設定ファイルを読ませる事も出来ないため、アップグレード後に手動で設定し直す必要が現時点ではありそうだ。

一方、plan-basedだとアップグレード後の修正コストが発生しないが、ytt overlayファイルを用意する必要があったり、デプロイ後の環境に対して適用する場合は個々に`kubectl edit`を実施する煩雑さが出てくる

class-basedがデフォルトとなり、plan-basedが廃れていきそうな感じはあるが、現時点ではユースケースに合わせて使い分けるのが良さそう。

## 余談：class-basedとplan-basedの`kubeadmconfigtemplates`
TKG2.1でクラスタ構築時に使う定義ファイルの書き方がclass-basedが新たに追加され、そちらがデフォルトとなり、従来の定義ファイルはplan-basedというタイプに分類され、レガシー扱いされるようになった。
これ、書き方が変わっただけのように見えなくもないが、実際は動作も異なっている。
以下、class-basedとplan-basedでクラスタを作成した状態で`kubeadmconfigtemplates`を取得した結果である。
```
$ kubectl get kubeadmconfigtemplates
NAME                                   AGE
tkg-vsphere-default-v1.0.0-md-config   2d16h　★ClusterClassと紐付き
tkg21wc-addcert-md-0                   8m22s　★plan-based
tkg21wc-md-0-bootstrap-ckrrb           9h　　　★class-based
```
class-basedの方は語尾にランダムな文字が付与されている。これはclass-basedだと`kubeadmconfigtemplates`が都度自動生成されることを意味する。
`MachineDeployment`リソースを修正すると、class-basedで作成したクラスタの場合、class-basedの`kubeadmconfigtemplates`の種である`tkg-vsphere-default-v1.0.0-md-config`をベースに新規に`kubeadmconfigtemplates`リソースが作成される。
そのため、従来型に慣れてる人で`kubeadmconfigtemplates`を修正していた人は手順が変わるため注意が必要である。