label-studio + Cloud Run + Litestreamで作る小規模向けアノテーションツール

## 背景
[label-studio](https://labelstud.io/)というアノテーションツールがあり、まずは個人利用のためローカルで利用していたのもつかの間、あっという間に利用ユーザ数が増えたのでちゃんとしたサービスにしようということでGoogle Cloud PlatformのCloud Runで運用しようというきっかけでlabel-studioサービスを立ち上げることになりました。

## 課題と対策
label studioの[gitリポジトリ](https://github.com/HumanSignal/label-studio/)を読んでいると「Run on Google Cloud」とあるので簡単にサービス立ち上げできるかなと思いきや、コスト最小化の制約をつけると以下の課題がわかった。

1. コスト最小化のため、CloudRunのインスタンス数を0にするとLabel-Studioサービス内で保存しているデータが削除される
1. データ永続化のために、CloudSQLを使うと月あたり100ドルを余裕で超えてくる

こういった課題からどうやったらデータを永続化しつつコスト最小でサービスを立ち上げることができるか？と検討した結果、[Litestream + CloudRun](https://zenn.dev/kou_pg_0131/articles/google-cloudrun-litestream)という構成でCloudRunサービスのデータ永続化できることがわかったので、これをlabel-studioに適用してみた。

## フロー
以下本サービス立ち上げに関して実施した手順である。
1. [方針の検討と決定](#方針の検討と決定)
1. [カスタムlabel-studioイメージの作成](#カスタムlabel-studioイメージの作成)
1. [ArtifactRegistryへの登録](#ArtifactRegistryへの登録)
1. [CloudRunのデプロイ](#CloudRunのデプロイ)
1. [カスタムドメインでの運用](#カスタムドメインでの運用)

## 方針の検討と決定
クラウドサービスを作るにあたって、やっぱりgitリポジトリを作成しその中にソースを入れ・・・といったことをやるととっても面倒なので、今回はGoogle Cloudコンソール上で必要なソースを取得し、そのソースの一部だけを変更しDockerイメージを作成する。
作成したDockerイメージをArtifactRegistryに登録し、それをCloudRunサービスとしてデプロイするというとってもシンプルな進め方にした。

## カスタムlabel-studioイメージの作成
以下の順番通り実施することで、カスタムlabel-studioイメージを作成しました。
### Google Cloudコンソールの立ち上げ
ブラウザ経由で進めてもいいが、ターミナルソフトで立ち上げるのが気に入っているので以下のコマンドでGoogle Cloudコンソールを起動する
```shell
gcloud cloud-shell ssh --authorize-session
```

### label-studioのソースを取得
以下のコマンドでlabel-studioのソースを取得する
```shell
git clone https://github.com/HumanSignal/label-studio
```

### litestreamの導入
label-studioはデフォルトでSQLite動作するのでそのあたりの変更は全くせず、デフォルトで保存されているSQLiteファイルの場所を確認し、以下のパスに保存されていることを確認した。
```shell
/label-studio/data/label_studio.sqlite3
```

この内容を用いて以下のようなlitestream.ymlをローカルPC内に作成する。Google Cloud Storage（以後：GCS）のパスは自分の環境に合わせて決めてください。
```litestream.yml
dbs:
  - path: /label-studio/data/label_studio.sqlite3
    replicas:
      - url: gcs://[Google Cloud Storageのバケット名]/[保存したいフォルダ名]
```

その後、litestream経由でlabel-studioを起動するシェルスクリプトentrypoint.shを以下のような感じでローカルPCに作成する。
```entrypoint.sh
#!/bin/bash
set -e

# when launching instance, remove sqlite file if it exists.
if [ -f /label-studio/data/label_studio.sqlite3 ]; then
  rm /label-studio/data/label_studio.sqlite3
fi

# Restore the database
litestream restore -if-replica-exists -config /etc/litestream.yml /label-studio/data/label_studio.sqlite3

# Run litestream with label-studio as the subprocess.
exec litestream replicate -exec "label-studio" -config /etc/litestream.yml

```

gitクローンによって取得したlabel-studioディレクトリ内にあるDockerfileを以下のように修正する。これはGoogle Cloud コンソール内なので、viを用いるかGoogle Cloud Shellエディタを用いて修正する。
修正した内容は以下の通り。
1. litestreamをgithubから取得→解凍→Dockerイメージ内/usr/local/binに展開
1. 先ほど作成したlitestream.ymlおよびentorypoint.shをDockerイメージ内にコピー
1. 環境変数「LABEL_STUDIO_ONE_CLICK_DEPLOY」を1に設定
必要に応じて「LABEL_STUDIO_DISABLE_SIGNUP_WITHOUT_LINK」や「LABEL_STUDIO_USERNAME」、「LABEL_STUDIO_PASSWORD」を設定して不用意にユーザ追加できないようにするのもあり。
1. 起動時にlabel-studioを立ち上げるのではなく、entorypoint.shを立ち上げるように変更
```shell
# syntax=docker/dockerfile:1
ARG NODE_VERSION=18
　：
(省略)
　：
RUN python3 label_studio/manage.py collectstatic --no-input && \
    chown -R 1001:0 $LS_DIR && \
    chmod -R g=u $LS_DIR

### =====================================================
# add litestream for SQLite replication
### -----------------------------------------------------
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz
COPY --chmod=755 web/new/entrypoint.sh /usr/bin/entrypoint.sh
COPY web/new/litestream.yml /etc/litestream.yml
ENV LABEL_STUDIO_ONE_CLICK_DEPLOY=1
### =====================================================

ENV HOME=$LS_DIR
　：
(省略)
　：
ENTRYPOINT ["./deploy/docker-entrypoint.sh"]

### =====================================================
# add litestream for SQLite replication
### -----------------------------------------------------
#CMD ["label-studio"]
CMD ["/usr/bin/entrypoint.sh"]
### =====================================================
```

### 作成したファイルをコピーや編集対象ファイル
litestream.ymlとentorypoint.shをGoogle Cloud Shellエディタに対して直接D&Dすることで所定のディレクトリにコピーできる。
ここでコピーする場所は以下の通りである。また編集するDockerfileの位置も同様に示す。
（補足：web以下に配置した理由は私のDocker知識があまりないせいでどうもCOPYでエラーになるため、COPYでエラーにならないディレクトリを探した結果webディレクトリ以下だったからである。これは後程解決したい問題の一つです）
```
/label-studio
  |___/web
  |     |____/new                   <---新規追加
  |            |___litestream.yml   <---新規追加
  |            |___entorypoint.sh   <---新規追加
  |___Dockerfile                    <---上書き修正
```

### Dockerイメージファイルの作成
カレントディレクトリを/label-studioに移動し、以下のコマンドを実行しDockerイメージを作成する
```shell
docker build -f Dockerfile -t label-studio .
```

### 起動確認など
実際に作ったDockerイメージを動かして無事litestreamのサブプロセスとしてlabel-studioが起動していることを確かめた。
```shell
docker run -it label-studio
```

## ArtifactRegistryへの登録
### リポジトリ作成
以下のように、Google Cloudコンソール上でArtifactRegistryサービスに移動し適切なリポジトリ名でリポジトリを作成する。ここではリージョンはasia-northeast1、リポジトリ名はlabel-studioとする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/663645f4-9556-536a-987e-3105d89bb8f8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/14ff3d2c-5e81-1dca-2cdb-01a1517cf396.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/d295c303-e4db-1a5c-fe99-8f4feb510f15.png)

### リージョンasia-northeast1のDockerリポジトリに対する認証を設定
以下のコマンドを実行し、Dockerリポジトリに対する認証設定をする。
```shell
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
```

### タグ付け
ビルドしたDockerイメージに対してArtifactRegistry用にタグ付けをする。
以下のコマンドを実行し、作成したDockerイメージのIMAGE IDを取得する
```shell
docker image list

REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
label-studio   latest    5064a45a61ea   16 seconds ago   1.46GB
```

そのIMAGE IDを用いて以下のコマンドでタグ付けをする。
```shell
docker tag 5064a45a61ea asia-northeast1-docker.pkg.dev/[GCPのプロジェクト名]/label-studio/202407261200

docker tag [IMAGE ID] [ArtifactRegistryの場所]/[GCPのプロジェクト名]/[リポジトリ名]/[リポジトリで利用するイメージ名]
```

### ArtifactRegistryへ登録
タグ付け後、以下のコマンドでArtifactRegistryに登録する。
```shell
docker push asia-northeast1-docker.pkg.dev/[GCPのプロジェクト名]/label-studio/202407261200
```

## CloudRunのデプロイ
以下のコマンドで直接Cloud Runサービスを立ち上げることができる。
```shell
gcloud run deploy label-studio --image asia-northeast1-docker.pkg.dev/[GCPのプロジェクト名]/label-studio/202407261200:latest

gcloud run deploy [リポジトリ名] --image [ArtifactRegistryの場所]/[GCPのプロジェクト名]/[リポジトリ名]/[リポジトリで利用するイメージ名]:[tag]
```


## GCPのアクセス権関係の付与
CloudRunで動作するlabel-studioサービスはlitestream.ymlで設定したGCSにアクセスするため、デプロイ後にGoogle Cloudコンソール上でCloudRunサービスにアクセスし、YAML内のserviceAccountNameからサービスアカウント名を確認し、そのサービスアカウントに対して権限を付与する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/12d27e92-d406-2849-288f-b83c851e94fb.png)
```サービスアカウントに付与した権限リスト.
storage.buckets.get
storage.buckets.getIamPolicy
storage.buckets.update
storage.objects.create
storage.objects.delete
storage.objects.get
storage.objects.getIamPolicy
storage.objects.list
storage.objects.update
resourcemanager.projects.get
resourcemanager.projects.list
storage.managedFolders.get
storage.managedFolders.list
```

## カスタムドメインでの運用
以下CloudRunサービスにある統合で「インテグレーションを追加」を押して「カスタムドメイン-Google Cloud Load Balancing」を選択する。あとは追加したいドメインを記述して作成することで、カスタムドメイン接続に必要な情報が生成されるので、それを利用する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/7292f522-1f92-2ca6-9b6e-fd8969fb4a84.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/8a87acee-1d9e-52ce-7c5f-f5d3318c0b1e.png)

カスタムドメインの設定後はLoad BalancerやCloud ArmorやIAPを設定すれば必要最低限のネットアクセスが可能なのでセキュアなサイト構築ができるので、そこは環境に合わせて設定するように。

## Label-Studioの設定
### アクセス権の設定
[label-studioのマニュアル](https://labelstud.io/guide/persistent_storage#Set-up-Google-Cloud-Storage)を参照し、クラウドストレージを利用する場合、必要に応じてアクセス権を付与する必要がある。

### GCSに対するCORSの設定
[label-studioのマニュアル](https://labelstud.io/guide/persistent_storage#Set-up-Google-Cloud-Storage)を参照し、こちらの環境下であるGCSのバケットに対してCORS設定を更新した。

それでもソースファイルを見に行った時に以下のエラーが生じた場合、ADCの設定を見直すとできるときがある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/5b1bb054-cfa1-b736-178e-e6c5f1a324b7.png)
実際に私が実施した内容は、設定したADC設定は以下の図に「Paste the contents of credentials.json here OR ...」書いてあるので、サービスアカウントのJSONを作成し、そのファイルを開いて中身をすべて選択しコピーした内容を「Google Application Credentials, leave blank to use default credentials(ADC)」にペーストした。結果、上記エラーが起きなくなった
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846192/d303f6bc-8186-54e5-3cf9-2f267573ec57.png)


## その後
以下のように環境変数を組み合わせることで、デプロイするだけで特定snapshotに復元してから立ち上げたりとかできるので、用途に合わせて組み込んでいくといい感じだった。
``` entrypoint.sh
# (抜粋)
  if [ "$USE_TARGET_GENERATION" = 'true' ]; then
    litestream restore -if-replica-exists -generation $TARGET_GENERATION -o /label-studio/data/label_studio.sqlite3 gcs://(GCSのパス)
  else
    litestream restore -if-replica-exists -config /etc/litestream.yml /label-studio/data/label_studio.sqlite3
  fi
```

``` ローカルで世代を確認するコマンド.
./litestream snapshots gcs://(GCSのパス)

replica  generation        index  size    created
gcs      0d97977e3789e113  0      780271  2024-08-13T07:07:22Z
gcs      28b7e7f8c4753701  0      780272  2024-08-20T01:24:12Z
```

以下のようにコマンドで実行してもよいし、Cloud Runコンソールで環境変数を設定してデプロイしても良い。
``` Cloud Runへのデプロイ.
gcloud run deploy [リポジトリ名] --image [ArtifactRegistryの場所]/[GCPのプロジェクト名]/[リポジトリ名]/[リポジトリで利用するイメージ名]:[tag] --set-env-vars "USE_TARGET_GENERATION=true" --set-env-vars "TARGET_GENERATION=(generationの値)"
```
<font color=red>※Cloud Runの環境変数に設定されるので、一度立ち上げた後はCloud Runコンソールからその環境変数を削除しておこう。</font>

# 動作確認環境 (2024/9/11追記)
label-studio：1.13.1.dev0
確認したブラウザ：Google Chrome 127.0.6533.0

上記追加した理由は新しいGoogle Chrome 128.x.x.xではプロジェクトの内容が描画されなかったため、label-studio側のアップデートが必要になったため
label-studio側をアップデートしてどうなるかは今後検討する。