はじめに Link to heading

Cloud Runのマルチコンテナ対応が発表されました。 個人開発など低予算な開発でSQLを使いたい場合に選択肢に上がるLitestreamですが、これまではCloud Runで利用する場合コンテナの中でLitestreamをメインプロセス、アプリケーションをサブプロセスとして動かしていました。

これはうまく動くものの、1コンテナに複数プロセスを同居させるのは原則に反するため気持ち悪い。dockerfileやスタートスクリプトもLitestreamに依存する形となってしまいます。

そこで、今回利用可能になったCloud Runのマルチコンテナ機能を利用してLitestreamをアプリケーションと分離したコンテナで動かしてみます。

これまで Link to heading

これまでのCloudRunではマルチコンテナをサポートしていなかったため、Running in the same containerに書いてあるように、Litestreamのプロセスからアプリケーションのプロセスを作成していました。

litestream replicate -exec "myapp -myflag myarg"

これは複数プロセスを1つのコンテナに載せるのと似たアプローチで、コンテナを利用する際に1つのコンテナにはプロセスは1つだけという原則を敷く事によって得られるメリットが失われてしまいます。 また、アプリケーション起動前にはrestoreをする必要があることも考えると、アプリケーションのDockerfileでスタートスクリプトを指定して、その中でrestore, replicate, アプリケーションの起動をすることになります。

#!/bin/sh
set -e

if [ -f /tmp/db.sqlite ]; then
  rm /tmp/db*
fi

litestream restore /tmp/db.sqlite
litestream replicate -exec "node ./server.js"

今のところこれがCloudRunでLitestreamを利用する主流のやり方だと思います。

これから Link to heading

CloudRunのマルチコンテナ対応により、LitestreamのKubernetesでの利用と近い構成を取ることができるようになりました。

すなわち、Litestreamの仕事であるDBの復元とバックアップは別コンテナに追い出すことで、アプリケーションコンテナはLitestreamのことを知らなくても良くなります。言い換えるとLitestreamへの依存を解消できます。例えばアプリケーションコンテナで問題が起きたときにそれがLitestreamの問題なのかアプリケーションの問題なのかを調べる必要がなくなるわけです。

またLitestreamのためのスタートアップスクリプトも不要になります。

service.yaml Link to heading

CloudRunのservice.yamlはこのようなかたちになりそうです。

アプリケーションとそのDockerfileは特別なことは何もしておらず、環境変数 DB_PATH にSQLiteのdbファイルが配置されるのでそれを利用して動作します。

GCSのバケット名やアプリケーションのimage名のところは読みかえてください。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: litestream-example
  annotations:
    run.googleapis.com/launch-stage: BETA
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/maxScale: "1"
        run.googleapis.com/container-dependencies: "{app: [restore], replicate: [restore]}"
    spec:
      containers:
      - image: docker.io/username/application
        name: app
        ports:
        - containerPort: 8080
        env:
        - name: DB_PATH
          value: "/var/lib/myapp/db"
        volumeMounts:
        - name: data
          mountPath: /var/lib/myapp 
      - image: litestream/litestream:0.3.9
        name: restore
        command: ['/bin/sh', '-c', '/usr/local/bin/litestream restore -if-db-not-exists -if-replica-exists -v -o /var/lib/myapp/db gcs://litestream-example/db && nc -lkp 8081 -e echo "restore completed!"']
        volumeMounts:
        - name: data
          mountPath: /var/lib/myapp
        startupProbe:
          tcpSocket:
            port: 8081
          initialDelaySeconds: 0
          failureThreshold: 30
          periodSeconds: 2
      - image: litestream/litestream:0.3.9
        name: replicate
        args: ['replicate', '/var/lib/myapp/db', 'gcs://litestream-example/db']
        volumeMounts:
        - name: data
          mountPath: /var/lib/myapp
      volumes:
      - name: data
        emptyDir:
          medium: Memory
          sizeLimit: 8Mi

気をつけるポイント Link to heading

共有領域 Link to heading

CloudRun実行中のみmemoryに領域を確保して、各コンテナに共有領域としてマウントします。 ここにSQLiteのファイルを置きます。 アプリケーション側はDBの場所を環境変数などで受け取れるように実装すると良いでしょう。

最大インスタンス数 Link to heading

Litestreamの制約から、Writeするクライアントは1つのみに制限されます。 そのため、最大インスタンス数を1に制限します。

restore Link to heading

Cloud StorageからSQLite3のファイルを手元に復元する処理はアプリケーションがスタートする前に実行される必要があります。 Kubernetesでは、initContainersの中で復元の処理を入れますが、CloudRunにはinitContainersがありません。 そこで、CloudRunの container-dependencies を利用して、restoreした後にアプリケーションが起動するよう設定します。 (2023-10-26 追記) restoreが完了してからアプリケーションが起動するためにはcontainer-dependenciesに加え、startupProbeを設定する必要がありました。

また、CloudRunのcontainers(sidecars)はどれか一つでも終了したら全体としても終了してしまうため、restoreが完了した後もrestoreコンテナは終了させずに動かし続ける必要があります。 ここでは、restoreが完了した後sleepしてコンテナを終了させずに保っています。 (2023-10-26 追記)startupProbeに応答するため、sleepではなくncでTCPに待ち受ける設定に修正しました。 もっと良い方法があれば教えてください。

まとめ Link to heading

アプリケーションからLitestreamへの依存を分離して動かすことができました。 これにより、アプリケーションとそのDockerfileはLitestreamを気にせず作ることができます。

CloudRunのマルチコンテナ機能は現在のところpreview段階なので、実運用は待ったほうが良いです。 またLitestreamは色々制約があるため、予算がある人は素直にCloudSQLを使ってください。