CVATをGKE上で動かしてみた話

ピリカでは撮影したごみの画像に対してアノテーションし、その結果を基に解析・研究を進めることがあります。

その中でGCP上の自社サービスと連携できるアノテーションツールが必要になり、CVATをGKEに構築しました。

<!-- more -->

## 前提事項

- Kubernetesに関し、一定の基礎知識がある前提で記載しています。
- 開発環境はmacOSで検証しています。

## CVATとは？

[CVAT(Computer Vision Annotation Tool)](https://www.cvat.ai/)はOpenCV teamが提供している、画像アノテーションツールです。以下の機能があります。

- Kubernetes上で、システムをマイクロサービスとして稼働できる
- セルフホスティングして稼働できる
- API, SDKが提供されており、APIを介してデータの操作が行える
  - このため、既存システムと連携して動かすことができる
- 事前学習モデルを基に、自動でアノテーションできる

同様のツールでlabelimg, annofabがあるものの、GCP上の他システムとの連携のしやすさからCVATを採用しました[^1]。

## 構築したいシステム構成

下図の様に、GCP上に構築している他システムと連携できるように構築することとしました。

![20230421084619.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967276/62746e13-f426-433a-41d3-e0c0740849a3.png)

ポイントは以下の点です。

- 保守のしやすさから、SSL証明書はマネージドのものを使います。
  - L7ロードバランサを作るので$0.6/日の費用がかかるものの、保守に掛かる手間を勘案して許容しています。
- HTTPからのアクセスはロードバランサで弾き、HTTPSのみ許容します。また、ドメインへのアクセスのみ許容します。

## 環境準備

Cloud SDKのインストールや、GCPのプロジェクト構築については割愛します。

本書の作成時点で、以下の構成で動作確認しています。

- Google Cloud SDK 409.0.0
  - gke-gcloud-auth-plugin 0.4.0
  - core 2022.11.04
  - Kubernetes v1.25.2-alpha+ae91c1fc0c443c464a4c878ffa2a4544483c6d1f
- [CVAT v2.3.0](https://github.com/opencv/cvat/releases/tag/v2.3.0) + PR#5505 + PR#5702
  - ※ 各PRが導入されたv2.4.2以上を利用することをおすすめします。
- Helm v3.10.2

また、本手順は公式マニュアルの下記手順をベースとしています。

https://opencv.github.io/cvat/docs/administration/advanced/k8s_deployment_with_helm

### (未実施であれば) GKE APIの有効化

以下のURLより、GKE APIを有効にします(GCPプロジェクトIDの箇所は適宜修正してください)。

`https://console.cloud.google.com/flows/enableapi?apiid=container.googleapis.com&hl=ja&project=(お使いのGCPプロジェクトID)`

### (未実施であれば) 必要ツールのインストール
#### helmのインストール

kubernetesまわりのパッケージマネージャのhelmがCVAT側で使われているので、インストールします。

```bash
$ brew install helm
$ helm version
version.BuildInfo{Version:"v3.10.2", GitCommit:"50f003e5ee8704ec937a756c646870227d7c8b58", GitTreeState:"clean", GoVersion:"go1.19.3"}
```

#### kubectlのインストール

Kubernetesのクラスターの操作に必要です。本ツールにより、pod, svc, ingress等のリソースを管理します。

```bash
$ brew install kubectl
$ kubectl version --client
version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.4",...(後略)}
```

#### Kubectl authentication pluginを導入する
GKE v1.26以降、Kubectl authentication pluginが認証ツールとして推奨されています。以下コマンドでインストールします。


```bash
$ gcloud components install gke-gcloud-auth-plugin
$ gke-gcloud-auth-plugin --version
Kubernetes v1.25.2-alpha+ae91c1fc0c443c464a4c878ffa2a4544483c6d1f
```

合わせて、環境変数にて同ツールが有効になるように.zshrcに書いておきましょう。

```bash
export USE_GKE_GCLOUD_AUTH_PLUGIN=True
```

## GKEへの構築方法
### クラスター作成

以下コマンドにより、クラスターを作成します(APIについては[公式リファレンス](https://cloud.google.com/sdk/gcloud/reference/container/clusters/create)参照)。5~10分くらい掛かるので待機します。

CVAT v2.4.0現在において、GKE Autopilot上では安定稼働しないためGKE Standardでクラスターを作成します。

※ ノード数が1以上あると、ノード稼働でCPUおよびメモリ使用量が常時発生します。もし課金を止めたい場合、ノード数を0にしてクラスターを停止させておくと良いです(ただし、引き続きストレージ容量は発生します)。


```bash
# machine-typeについて、改行直前にスペースがあるとバリデーションエラーになるので注意
$ gcloud container clusters --project your-project-id\
  create cvat-cluster\
  --zone asia-northeast1-c\
  --node-locations asia-northeast1-c\
  --num-nodes 1\
  --max-nodes 1\
  --min-nodes 0\
  --disk-size 25\
  --machine-type e2-custom-2-4096\
  --enable-network-policy\
  --service-account=cvat@your-project-id.iam.gserviceaccount.com
```

上記では、以下の設定でクラスタを生成します。

- ゾーン、ノードの場所: 東京リージョン(c)
- ノード数: 1 (最大1, 最小0)
- ブートディスクの永続ディスク容量: 25GB
  - ディスク容量は後からシュリンクできないため、小さめに定義しています。
- マシンタイプ: e2インスタンス(2 vCPU, 4GB)
  - e1-standardでvCPU=1だとCPU不足で起動できません。
  - e2-standard-1だとメモリ不足になり、e2-standard-2だとメモリが8GB余るためメモリ使用量をカスタムしています。

無事に作成できていれば、以下のコマンドで作成したクラスターを確認できます。

```bash
$ gcloud container clusters --project your-project-id list
NAME               LOCATION           MASTER_VERSION    MASTER_IP       MACHINE_TYPE      NODE_VERSION      NUM_NODES  STATUS
cvat-cluster asia-northeast1-c  1.24.10-gke.2300  xx.xxx.xxx.xxx  e2-custom-2-4096  1.24.10-gke.2300  1          RUNNING
```

### クラスターへのアクセス認証

以下のコマンドにより、作成したクラスターへの認証情報を取得できます。

```bash
$ gcloud container --project your-project-id \
  clusters get-credentials cvat-cluster \
  --zone asia-northeast1-c
```

認証情報が取れていれば、`kubectl get pods -A`コマンドにより今稼働している全名前空間のpod一覧を確認できます。

```bash

$ kubectl get pods -A
NAMESPACE     NAME                                                          READY   STATUS    RESTARTS   AGE
kube-system   calico-node-k44vh                                             1/1     Running   0          8m58s
kube-system   calico-node-vertical-autoscaler-7fd97448b6-jv97h              1/1     Running   0          10m
kube-system   calico-typha-5d4dc54774-xb4gz                                 1/1     Running   0          9m3s
...
```

### ネットワーク関連の設定
#### GoogleマネージドSSL証明書の準備

traefikの代わりにGKE Ingressを利用します。

1. 以下の通り、cert.yamlファイルを生成します。metadata.nameが証明書の名前になります。

**cert.yaml**

```yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: cvat-managed-cert
spec:
  domains:
    - cvat.your.domain
```


2. 以下コマンドにより、GoogleマネージドSSL証明書を作成します。

```bash
$ kubectl apply -f cert.yaml
```

作成後、以下のコマンドで作成できたことを確認します。

```bash
$ kubectl describe managedcertificate  cvat-managed-cert

Spec:
  Domains:
    cvat.your.domain
Status:
  Certificate Name:    mcrt-***
  Certificate Status:  Provisioning
  Domain Status:
    Domain:     cvat.your.domain
    Status:     Active
  Expire Time:  ***
...

```

CVATデプロイ後、正常に稼働していれば上記Certificicate StatusがActiveになります。

※ 当該ドメインおよび静的アドレスについて、DNSツール上でAレコードで紐付け設定してください。

### グローバルな静的IPアドレスを予約する

注意: 予約した状態で使用されていない場合、静的IPアドレスの予約費用がかかります。そのため、この作業が終わり次第、後述のデプロイまで終わらせてください。

1. グローバルな静的IPアドレスを作ります。

```bash
$ gcloud compute addresses create your-cvat-static-ip --global --project your-project-id
```

2. 作成できたか確認します(STATUS = RESERVEDになっているはずです)

```bash
$ gcloud compute addresses list --global --project your-project-id

NAME                               ADDRESS/RANGE   TYPE      PURPOSE  NETWORK  REGION  SUBNET  STATUS
your-cvat-static-ip  ***.***.***.***  EXTERNAL                                    RESERVED
```

### CVATのデプロイ用テンプレート設定値の準備

cvatのv2.3.0をチェックアウトします。

その後、以下のファイルを編集します。

**Charts.yaml**

```diff
-    condition: ingress.enabled
+     condition: traefik.enabled   # GCE Ingressを利用できるよう、values側で指定があればtraefikが立ち上がるようにします。
```

**values.override.yaml**

以下の設定を行います。

- backend, frontendサービスをNodePortタイプにします。
  - GKE Ingressから両サービスのIPが特定するためです。
- postgresqlの保守用パスワードを記載します(適宜決定ください)。
- Ingressについて、ルーティング設定およびHTTPSや静的アドレスの設定を入れます。
  - kubernetes.io/ingress.global-static-ip-name: 予約した静的IPアドレス名を指定します。
  - networking.gke.io/managed-certificates: 予約したマネージド証明書名を指定します。
  - kubernetes.io/ingress.allow-http: HTTPプロトコルでのアクセスを禁止します。
  - kubernetes.io/ingress.class: GKE Ingressを使います。
  - hostにて、特定ドメインからのみアクセスを許可します。
- traefikをオフにします。
  - cvat-traefikサービスが作られず、これにともないL4LBが生成されません。代わりに、GCE IngressによりL7LBが作成され、上述のマネージド証明書が割り当てられます。

```yaml
cvat:
  backend:
    server:
      envs:
        USE_ALLAUTH_SOCIAL_ACCOUNTS: "false"  # SSOログインはv2.3.0では利用できない
    service:
      spec:
        type: NodePort
  frontend:
    service:
      type: NodePort

postgresql:
  secret:
    password: 利用パスワード
    postgres_password: superuserの生パスワード
    replication_password: レプリケーション用パスワード

ingress:
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "your-cvat-static-ip"   # 予約した静的IPアドレス名を指定する
    networking.gke.io/managed-certificates: "cvat-managed-cert"   # 予約したマネージド証明書名を指定する
    kubernetes.io/ingress.allow-http: "false"   # HTTPSでの接続のみ許可する
    kubernetes.io/ingress.class: gce    # GCE Ingressを使うよう設定する
  hosts:
    - host: "cvat.your.domain"
      paths:   # 以下、values.yamlからの変更はなし
      - path: /api
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /admin
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /static
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /django-rq
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /git
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /opencv
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path: /profiler
        pathType: "Prefix"
        service:
          name: backend-service
          port: 8080
      - path : /
        pathType: "Prefix"
        service:
          name: frontend-service
          port: 80

traefik:
  enabled: false   # GCE Ingressを使うため、traefikを無効化する
```

**templates/cvat_backend/service.yml**

cvat_backendのLBからのヘルスチェックパスを修正します。

```diff
  annotations:
+    cloud.google.com/backend-config: '{"default":"{{ .Release.Name }}-backend-server-healthcheck"}'
    {{- with .Values.cvat.backend.service.annotations }}
```

**templates/cvat_backend/backend_config.yml**

同上です。metadata.nameはservice.ymlの`cloud.google.com/backend-config`に渡している値と合わせます。

```yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: {{ .Release.Name }}-backend-server-healthcheck
spec:
  healthCheck:
    requestPath: /api/server/health/?format=json
```

### デプロイ

コンテキストを確認します。

```bash
$ kubectl config current-context
```

**実行例**

以下のように、`gke_(GCPプロジェクト名)_(ゾーン名)_(クラスタ名)`という形でcontextが表示されます。

```bash
gke_your-project-id_asia-northeast1-c_cvat-cluster
```

依存関係のインストールを行います(helm-chartディレクトリで実施してください)。

```bash
$ helm dependency update
```

以下コマンドにより、デプロイを行います(CVATのルートディレクトリで実施してください)。

```bash
$ helm upgrade -n default cvat \
  -i --create-namespace ./helm-chart \
  -f ./helm-chart/values.yaml \
  -f ./helm-chart/values.override.yaml
```

実行後、以下の様な表示ができていればデプロイ完了です！

```bash
NAME: cvat
LAST DEPLOYED: Thu Dec  1 15:48:39 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
```

## 事後確認
### pod
以下の様に、cvat-backend, cvat-frontend, cvat-opa, cvat-postgresql, cvat-redis-*ごとのpodsが立ち上がります。すべてSTATUSがRUNNINGであればOKです。

```bash
$ kubectl get pod

NAME                                            READY   STATUS    RESTARTS   AGE
cvat-backend-server-xxx-xxx            1/1     Running   0          2d7h
cvat-backend-utils-xxx-xxx               1/1     Running   0          2d7h
cvat-backend-worker-default-xxx-xxx     1/1     Running   0          2d7h
cvat-backend-worker-default-xxx-xxx     1/1     Running   0          2d7h
cvat-backend-worker-low-xxx-xxx        1/1     Running   0          2d7h
cvat-backend-worker-webhooks-xxx-xxx   1/1     Running   0          2d7h
cvat-frontend-xxx-xxx                  1/1     Running   0          2d7h
cvat-opa-xxx-xxx                       1/1     Running   0          2d7h
cvat-postgresql-0                               1/1     Running   0          2d7h
cvat-redis-master-0                             1/1     Running   0          2d7h
cvat-redis-replicas-0                           1/1     Running   0          2d7h
cvat-redis-replicas-1                           1/1     Running   0          2d7h
cvat-redis-replicas-2                           1/1     Running   0          2d7h
```

### ingress
以下の様に、HOSTSにvalues.override.yamlで設定したドメイン、ADDRESSに確保した静的IPアドレスが入っていればOKです。

```bash
$ kubectl get ingress

NAME   CLASS    HOSTS                                        ADDRESS         PORTS   AGE
cvat   <none>   cvat.your.domain   xx.xxx.xxx.xxx   80      13d
```

### 静的IPアドレス
デプロイ後、数分経ってから確認します。IngressとIPが紐付いていれば、`IN_USE`になっています。
```bash
$ gcloud compute addresses list --global

NAME                               ADDRESS/RANGE   TYPE      PURPOSE  NETWORK  REGION  SUBNET  STATUS
your-cvat-static-ip  ***.***.***.***  EXTERNAL                                    IN_USE
```

### 証明書
デプロイしてから、最長24時間待つ必要があります。ドメインからCVATにアクセスできていれば、証明書およびドメインともにActiveになります。

### ページ表示
HTTPSで設定したドメインにアクセスして、以下の画面が出ていればOKです。

なお、CVATをデプロイした直後は起動中のため、502エラーが発生することがあります。

![20230420172753.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967276/0d682f01-2dd4-1992-0f68-643581a92d68.png)

## Q&A
### CVATのデータをバックアップしたい
以下ページにあるとおり、CVATのデータはpostgresqlにある分とバックエンド上にある分の2つがあります。これら2つのバックアップを取っていれば、後で復元することができます。

GKEの場合、以下の様にすることでpodから永続データのバックアップができます。

```
$ kubectl cp cvat-backend-server-xxx-xxx:/home/django/data ~/your/backup/dir/server/data
$ kubectl cp cvat-postgresql-0:/bitnami/postgresql/data ~/your/backup/dir/postgresql/data --retries 10   # データ量が数百MBある場合があり失敗する可能性があるため、通信リトライを入れている
```

https://opencv.github.io/cvat/docs/administration/advanced/backup_guide/

[^1]:  labelimgの後方互換ツールで[Label Studio](https://labelstud.io/)が開発されており、そちらもCVATとほぼ同等の機能が利用可能です。こちらはCloud Runを使ってホスティングすることができます。