GCP の Spanner を利用するアプリケーションの検証環境として Cloud Run のサイドカーで Spanner Emulator を起動できるか試した。
ポイントは2つ。
- startup probe を利用して Spanner Emulator の初期化を待つ
SIGTERM
をトラップして Spanner Emulator のデータをダンプする
以下ではこの2点を中心に、Cloud Run のサイドカーで Spanner Emulator を動かすための設定につい説明する。
Cloud Run の基本的な部分などは省略する。
Cloud Run の startup probe
Cloud Run には startup probe という仕組みがあり、コンテナの初期化が終了しているかどうかを伝えることができる。
これを使うとコンテナの初期化処理が終了するまでコンテナを実行し続けることができる。
startup probe には TCP, HTTP, gRPC の3つの方法がある。
HTTP を使うとコンテナで HTTP/2 を使えなくなる。
TCP を使った方法では接続が確立できれば初期化終了と判断されるので何もレスポンスを返す必要はない。
デフォルトの startup probe は次の設定になっている。
1
2
3
4
5
6
|
startupProbe:
tcpSocket:
port: CONTAINER_PORT
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
|
CONTAINER_PORT
は Cloud Run で実行しているサービスのポート。
プロトコルは TCP を利用しているので、前述の通り接続が確立できた時点で初期化終了と判断される。
従って、Cloud Run のコンテナで Spanner Emulator を単純に実行した場合、
エミュレータが起動した時点、データベース作成や初期データの挿入前に初期化終了と判断されてしまう。
実際に startup probe を設定せずに試したときは、
データベース作成や初期データの挿入が終了する前に初期化終了と判断され、
コールドスタートのためにコンテナがシャットダウンされているようだった。
今回は TCP を使って startup probe を設定する。
Cloud Run からの TCP 接続を待ち受けるのに nc (netcat-openbsd) を使う。
初期化処理終了後に次のコマンドで TCP 接続を待ち受ける。
1
|
timeout 60 nc -dklv -w 0 8081 2>&1 &
|
nc
によるリクエストの処理は startup probe で初期化終了を通知できれば不要なので timeout 60
で終了させる。
接続確立だけできればよいので、nc
が標準入力から何も読み込まないように -d
を指定し、
接続確立後すぐに終了するように -w 0
を指定している。
こちらによると、
Cloud Run がコンテナに SIGTERM
を送信してからコンテナが終了させられるまでに10秒の猶予が与えられている。
従って SIGTERM
をトラップして Spanner Emulator のダンプ処理を実行すれば、
実際にコンテナが終了する前に Spanner Emulator のデータを退避できる。
Spanner Emulator を起動するサイドカーの Dockerfile は次の内容で作成した。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
FROM golang:1.25.0-alpine3.22 AS builder
RUN go install 'github.com/cloudspannerecosystem/spanner-dump@latest'
FROM google/cloud-sdk:535.0.0-emulators
COPY --from=builder /go/bin/spanner-dump /usr/local/bin/
RUN apt-get update && \
apt-get install -y --no-install-recommends netcat-openbsd && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
gcloud components install alpha spanner-cli --quiet
WORKDIR spanner
COPY spanner-ddl.sql spanner-init.sql ./
COPY --chmod=755 docker-entrypoint.sh ./
EXPOSE 8081
ENTRYPOINT ["./docker-entrypoint.sh"]
|
2つのツールをインストールしている。
1つは Spanner Emulator からデータをダンプするために使う spanner-dump。
もう1つは startup probe のリクエストを処理するために使う netcat-openbsd。
spanner-init.sql
はダンプデータが存在しない場合に利用する初期化ファイル。
ENTRYPOINT
で指定した docker-entrypoint.sh
の内容は次の通り。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#!/bin/bash
set -e
trap cleanup HUP INT QUIT TERM
cleanup() {
spanner-dump -p my-project -i my-instance -d my-database --tables Singers,Albums --no-ddl >spanner-dump.sql
export CLOUDSDK_ACTIVE_CONFIG_NAME=default
gcloud storage cp spanner-dump.sql gs://${SPANNER_DUMP_BUCKET}/spanner-dump.sql
test -n "$SPANNER_PID" && kill -s 0 $SPANNER_PID && kill -s KILL $SPANNER_PID
}
main() {
gcloud storage cp gs://${SPANNER_DUMP_BUCKET}/spanner-dump.sql . || echo 'not found spanner-dump.sql'
gcloud beta emulators spanner start --host-port 0.0.0.0:9010 &
SPANNER_PID=$!
gcloud config configurations create emulator
gcloud config set auth/disable_credentials true
gcloud config set project my-project
yes | gcloud config set api_endpoint_overrides/spanner http://localhost:9020/
export SPANNER_EMULATOR_HOST=localhost:9010
gcloud spanner instances create my-instance --config=emulator-config --description="Test Instance" --processing-units=100
gcloud spanner databases create my-database --instance my-instance --ddl-file spanner-ddl.sql
if [[ -f spanner-dump.sql ]]; then
gcloud alpha spanner cli my-database --project my-project --instance my-instance --source spanner-dump.sql
# Delete the file when it is no longer needed, because writing to the file system consumes memory.
rm spanner-dump.sql
elif [[ -f spanner-init.sql ]]; then
gcloud alpha spanner cli my-database --project my-project --instance my-instance --source spanner-init.sql
fi
# startup probe
timeout 60 nc -dklv -w 0 8081 2>&1 &
wait $SPANNER_PID
}
if [ $# -eq 0 ]; then
main
else
exec "$@"
fi
|
docker-entrypoint.sh
の main
関数では最初に Spanner Emulator のダウンロードを試みる。
1
|
gcloud storage cp gs://${SPANNER_DUMP_BUCKET}/spanner-dump.sql . || echo 'not found spanner-dump.sql'
|
Spanner Emulator はバックグラウンドで実行し、そのプロセス ID を SPANNER_PID
に保持しておく。
1
2
|
gcloud beta emulators spanner start --host-port 0.0.0.0:9010 &
SPANNER_PID=$!
|
spanner-dump.sql
のダウンロードに成功していればそれを使って初期化し、
存在しなければイメージに含めておいた spanner-init.sql
を使って初期化する。
1
2
3
4
5
6
7
|
if [[ -f spanner-dump.sql ]]; then
gcloud alpha spanner cli my-database --project my-project --instance my-instance --source spanner-dump.sql
# Delete the file when it is no longer needed, because writing to the file system consumes memory.
rm spanner-dump.sql
elif [[ -f spanner-init.sql ]]; then
gcloud alpha spanner cli my-database --project my-project --instance my-instance --source spanner-init.sql
fi
|
こちらによると、
ファイルシステムへの書き込みはメモリを消費するようなので、不要になった spanner-dump.sql
は削除している。
wait $SPANNER_PID
でコンテナが終了しないようにする。
Cloud Run にデプロイするための YAML ファイルは次のような内容で作成する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
annotations:
name: cloudrun-spanner-emulator-app
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/maxScale: '1'
spec:
containers:
- image: asia-northeast1-docker.pkg.dev/${PROJECT_ID}/gcp-examples/cloudrun-spanner-emulator-app:1.0.0
ports:
- containerPort: 8080
env:
- name: SPANNER_EMULATOR_HOST
value: localhost:9010
- image: asia-northeast1-docker.pkg.dev/${PROJECT_ID}/gcp-examples/cloudrun-spanner-emulator:1.0.0
startupProbe:
tcpSocket:
port: 8081
initialDelaySeconds: 10
timeoutSeconds: 1
failureThreshold: 12
periodSeconds: 5
serviceAccountName: cloudrun-spanner-emulator-app@${PROJECT_ID}.iam.gserviceaccount.com
|
Spanner Emulator をサイドカーにした検証環境なので autoscaling.knative.dev/maxScale: '1'
で最大スケールを1にしている。
イメージとして asia-northeast1-docker.pkg.dev/${PROJECT_ID}/gcp-examples/cloudrun-spanner-emulator:1.0.0
を指定したのがサイドカーコンテナ。
こちらによると
startup probe は最大240秒までしか待たないので、failureThreshold * periodSeconds
が240以下になるようにする。
アプリケーション用のコンテナ(cloudrun-spanner-emulator-app)は Spanner Emulator に読み書きする適当なものを作成した。
デプロイして試したところ、うまく Spanner Emulator を初期化できた。
データのダンプとリストアも機能した。