VeleroのHook機能でHarborをRead Onlyに変更してバックアップを取得する

HarborのバックアップをVeleroで取得する際、HarborをRead Onlyにしてからバックアップしたいという人もいると思う。

HarborをRead OnlyにするにはConfigurationの`Repository Read Only`にチェックを入れないといけないが、Veleroによる定期バックアップ時にこれをチェックしたいと思うと、UIからチェックを入れるのは現実的ではない。
![1675923368838.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/709ce6d2-67a8-d24c-39d3-c64009dffba4.png)
今回、VeleroのBackup HooksとHarborのAPIを組み合わせてバックアップ時に自動でRead Onlyの有効化・解除を実行してみる。

## HarborのAPI確認

Harborにログインすると、左下にAPI Explorerへのリンクが表示される。
![1675923625431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/fb066030-210b-8028-da6a-a64fbb86869d.png)
こちらの中にある、PUT /configurationsを利用してRead Onlyに変更する。
![1675923670215.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/d63d4ccb-b40d-df5b-f7d9-37d02e748962.png)
具体的には以下のようにAPIを発行し、Read Onlyに変更する。

```
export HARBOR_URL="myharbor.info"
export HARBOR_USER=fuga
export HARBOR_PASSWORD=hoge
curl -X 'PUT' \
  https://$HARBOR_URL/api/v2.0/configurations \
  -H 'Content-Type: application/json' \
  -d '{"read_only":true}' -u $HARBOR_USER:$HARBOR_PASSWORD
```
実際に実行すると、問題なければ標準出力には何も表示されずcurlは終了し、Harborにアクセスすると以下のようになる。
![1675924030802.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/2eab3a2c-bf26-8203-db1d-2c8d82a18892.png)

`Repository Read Only`がチェックされ、UI上でread-only modeである旨が表示される。
解除するには`{"read_only":false}`でAPIを発行する。

## Backup Hooksの設定
[Backup Hooks](https://velero.io/docs/v1.10/backup-hooks/)はVeleroでバックアップする際、バックアップ前後に処理を仕込める機能である。大きく、バックアップ前に処理を追加するPre hooksと、バックアップ後に処理を追加するPost hooksに大別出来る。
この機能は対象のPodのAnnotationに追記することで、Podのバックアップ時に機能が動くようになる。

以下、機能をざっくり紹介する。なお、全てOptionalで必須なものはない。

Pre hooks
|機能|意味|
|---|---|
|pre.hook.backup.velero.io/container|コマンドを実行するコンテナを指定する。指定しない場合、Podの最初のコンテナが選ばれる。ここで指定する名前はPod内のいわゆる`spec.containers.name`を指定する必要があるため、動かすコンテナは予めPodに取り込んでおく必要がある。|
|pre.hook.backup.velero.io/command|実行するコマンド。オプションが必要な場合は["/usr/bin/uname", "-a"]のようなJSON配列で指定する。|
|pre.hook.backup.velero.io/on-error|コマンド失敗時に処理を継続するかどうかの指定で、デフォルトはFail（処理停止）となる。|
|pre.hook.backup.velero.io/timeout|コマンドの実行のタイムアウト値で、デフォルトで30秒。タイムアウト値を超えるとエラーとなる。|

Post hooks
|機能|意味|
|---|---|
|post.hook.backup.velero.io/container|バックアップ後に動く以外はPreと同じ|
|post.hook.backup.velero.io/command|バックアップ後に動く以外はPreと同じ|
|post.hook.backup.velero.io/on-error|バックアップ後に動く以外はPreと同じ|
|post.hook.backup.velero.io/timeout|バックアップ後に動く以外はPreと同じ|

これを設定する前に、Harbor側にコマンドを実行するコンテナを本来は仕込む必要がある。
ただ、今回はregistryのPod内のコンテナがcurlを持っていたため、コマンド実行用のコンテナを別で用意する必要がなく、特にコンテナは追加せずに既存のものを利用する。


今回はHarborはHelmでインストールしているため、Deploymentを修正してPodを追加する。
ここではHelmでインストールしたHarborのPodにAnnotationを追加する。（Tanzu Packagesでインストールしている場合はAppリソースのReconsileが走るので、戻されないように仕込む必要あり）
deploymentをeditする。
```
kubectl edit deploy harbor-registry
```
以下のような感じで、PodのAnnotationでPrehookでRead Onlyの有効化、PosthookでRead Onlyの解除を設定する。
```yaml
  template:
    metadata:
      annotations:
        pre.hook.backup.velero.io/command: '["bash", "-c", "echo {\\\"read_only\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1"]'
        pre.hook.backup.velero.io/container: registry
        post.hook.backup.velero.io/command: '["bash", "-c", "echo {\\\"read_only\\\":false}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1"]'
        post.hook.backup.velero.io/container: registry
```
元のコマンドだとシングルクォートで作っていたが、Annotation全体をシングルクォートで括る必要があるため、シングルクォートでデータが渡せない。そのため、データのjsonを一回ファイルに吐いてファイルから読み込ませる形でシングルクォートの利用を回避している。（ここでかなり時間を食った。。。）
編集して保存すると、Annotationが入ったPodが作成される。
```
$ kubectl get pod harbor-registry-76b7d85bcd-6pkfg  -n harbor -o jsonpath={.metadata.annotations} | jq . |grep hook
  "post.hook.backup.velero.io/command": "[\"curl\", \"-XPUT https://myharbor.info/api/v2.0/configurations -H \\\"Content-Type: application/json\\\" -d {\"read_only\":false} -u imurata:VMware1!\"]",
  "post.hook.backup.velero.io/container": "registry",
  "pre.hook.backup.velero.io/command": "[\"curl\", \"-XPUT https://myharbor.info/api/v2.0/configurations -H \\\"Content-Type: application/json\\\" -d {\"read_only\":true} -u imurata:VMware1!\"]",
  "pre.hook.backup.velero.io/container": "registry"
```
なお、タイミングによっては新規Podの起動の方が先に走って既存Podが落ちず、PVのアタッチに失敗することがあったので、その場合は古いReplicaSetを削除するとよい。
```
kubectl delete rs -n harbor harbor-registry-7f5b6b9b69
```
これで仕込みは完了となる。

## バックアップの取得

バックアップを取得する。
```
velero backup create harbor-backup --include-namespaces harbor --wait
```
バックアップのログを確認する。
```
velero backup logs harbor-backup | less
```
ログを見ると、コマンドを実行していることが分かる。
```
time="2023-02-09T12:38:56Z" level=info msg="running exec hook" backup=velero/harbor-backup hookCommand="[bash -c echo {\\\"read_only\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1]" hookContainer=registry hookName="<from-annotation>" hookOnError=Fail hookPhase=pre hookSource=annotation hookTimeout="{30s}" hookType=exec logSource="pkg/podexec/pod_command_executor.go:126" name=harbor-registry-66547c6fbb-rxj5c namespace=harbor resource=pods
time="2023-02-09T12:38:56Z" level=info msg="stdout: " backup=velero/harbor-backup hookCommand="[bash -c echo {\\\"read_only\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1]" hookContainer=registry hookName="<from-annotation>" hookOnError=Fail hookPhase=pre hookSource=annotation hookTimeout="{30s}" hookType=exec logSource="pkg/podexec/pod_command_executor.go:173" name=harbor-registry-66547c6fbb-rxj5c namespace=harbor resource=pods
time="2023-02-09T12:38:56Z" level=info msg="stderr:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    18    0     0  100    18      0     98 --:--:-- --:--:-- --:--:--    98\n" backup=velero/harbor-backup hookCommand="[bash -c echo {\\\"read_only\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1]" hookContainer=registry hookName="<from-annotation>" hookOnError=Fail hookPhase=pre hookSource=annotation hookTimeout="{30s}" hookType=exec logSource="pkg/podexec/pod_command_executor.go:174" name=harbor-registry-66547c6fbb-rxj5c namespace=harbor resource=pods
time="2023-02-09T12:38:56Z" level=info msg="Executing custom action" backup=velero/harbor-backup logSource="pkg/backup/item_backupper.go:321" name=harbor-registry-66547c6fbb-rxj5c namespace=harbor resource=pods
```
本当にRead Onlyになったのかが分かりづらいので、気になる人は試しにPostにも`{\\\"read_only\\\":true}`を書いて試してみるとよい。
その場合、バックアップ後に以下の様な画面になって、Read Onlyになっていることが確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/072250da-a489-1cc6-61ec-b230ba5e7d4c.png)


## 補足：TKG版HarborへのAnnotationの追加方法
TKGのHarborは`kind: App`というリソースで全てのリソースがリコンサイルするよう守られているため、Deploymentに修正を入れても元に戻ってしまう。
これを突破するためには`kind: PackageInstall`という`kind: App`の親玉に対してyttでoverlayするファイルを読ませてあげる必要がある。
ここはCarvelのyttおよび`kind: Packages`周りの知識がないと理解できないが、今回はその説明は省略する。
公式ドキュメントは[このあたり](https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/1.6/vmware-tanzu-kubernetes-grid-16/GUID-ytt.html#applying-overlays-to-packages-3)が参考になりそうであまり参考にならない。

最初にyttのoverlay用ファイルを作成する。
```yaml
cat << EOF > ./overlay.yaml
#@ load("@ytt:overlay", "overlay")
#@ load("@ytt:data", "data")
#@overlay/match by=overlay.and_op(overlay.subset({"kind": "Deployment"}), overlay.subset({"metadata": {"name": "harbor-registry"}}))
---
spec:
  template:
    metadata:
      #@overlay/match missing_ok=True
      annotations:
        pre.hook.backup.velero.io/command: '["bash", "-c", "echo {\\\"read_only\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1"]'
        pre.hook.backup.velero.io/container: registry
        post.hook.backup.velero.io/command: '["bash", "-c", "echo {\\\"read_only\\\":false}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \"Content-Type: application/json\" -d @/tmp/data.json -u vmware:VMware1VMware1"]'
        post.hook.backup.velero.io/container: registry
EOF
```
作成したoverlayが適切にDeploymentにはまるか確認する。
```
 kubectl get deploy -o yaml -n tanzu-system-registry harbor-registry | ytt -f ./overlay.yaml -f -
```
ここでDeploymentの`spec.template.metadata.annotations`に追記できていることを確認する。
問題なければ作成したoverlay.yamlをSecretとして登録する。なお、Secretの作成NamespaceはHarbor Packageをインストールした際のNamespaceとなる。Harbor PackageをインストールしたNamespaceは以下で確認できる。
```
$ kubectl get pkgi -A
NAMESPACE     NAME           PACKAGE NAME                    PACKAGE VERSION         DESCRIPTION           AGE
my-packages   cert-manager   cert-manager.tanzu.vmware.com   1.7.2+vmware.1-tkg.1    Reconcile succeeded   11h
my-packages   contour        contour.tanzu.vmware.com        1.20.2+vmware.1-tkg.1   Reconcile succeeded   11h
my-packages   harbor         harbor.tanzu.vmware.com         2.5.3+vmware.1-tkg.1    Reconcile succeeded   11h
```
ここでは`my-packages`Namespaceにインストールしていたので、以下でSecretを作成する。
```
kubectl create secret generic harbor-overlay --from-file=overlay.yaml -n my-packages
```
作成したOverlayを読み込ませる。
```
kubectl annotate PackageInstall harbor ext.packaging.carvel.dev/ytt-paths-from-secret-name.0=harbor-overlay -n my-packages
```
これを実行すると、パッケージ構成が変わったため、Harborの`kind: App`リソースでリコンサイルが走る。
```
$ kubectl get app -A
NAMESPACE     NAME           DESCRIPTION           SINCE-DEPLOY   AGE
my-packages   cert-manager   Reconcile succeeded   90s            11h
my-packages   contour        Reconcile succeeded   9m             11h
my-packages   harbor         Reconciling           6s             11h
```
書き換えに成功すると、`kind: App`リソースの状態が`Reconcile succeeded`になる。
```
$kubectl get app -n my-packages harbor
NAME     DESCRIPTION           SINCE-DEPLOY   AGE
harbor   Reconcile succeeded   91s            11h
```
PodのAnnotationを見ると、適切に入っていることが分かる。
```json
$ kubectl get pod -n tanzu-system-registry harbor-registry-77cc4cbff-5k7kg -ojsonpath={.metadata.annotations} | jq .
{
  "kubernetes.io/psp": "vmware-system-privileged",
  "post.hook.backup.velero.io/command": "[\"bash\", \"-c\", \"echo {\\\\\\\"read_only\\\\\\\":false}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \\\"Content-Type: application/json\\\" -d @/tmp/data.json -u vmware:VMware1VMware1\"]",
  "post.hook.backup.velero.io/container": "registry",
  "pre.hook.backup.velero.io/command": "[\"bash\", \"-c\", \"echo {\\\\\\\"read_only\\\\\\\":true}>/tmp/data.json && curl -XPUT https://myharbor.info/api/v2.0/configurations -H \\\"Content-Type: application/json\\\" -d @/tmp/data.json -u vmware:VMware1VMware1\"]",
  "pre.hook.backup.velero.io/container": "registry"
}
```