Cloud Run サイドカーで Spanner Emulator を動かす

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 のコンテナ終了プロセス

こちらによると、 Cloud Run がコンテナに SIGTERM を送信してからコンテナが終了させられるまでに10秒の猶予が与えられている。 従って SIGTERM をトラップして Spanner Emulator のダンプ処理を実行すれば、 実際にコンテナが終了する前に 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.shmain 関数では最初に 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 にデプロイ

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 を初期化できた。 データのダンプとリストアも機能した。