はじめに 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を使ってください。