AI・機械学習チームの中村伊吹(@inakam00)です。 この記事はAI・機械学習チームブログリレー6日目の記事です。
最近はNetflixでコナンの映画が配信されるようになり、毎日コナンの映画を1つずつ消化するのが日課になっています。 『探偵たちの鎮魂歌』はギミックがわかりやすくてお気に入りです。
前日は高橋さんの『Agentic Coding時代のデータ分析環境: marimo + gokartで高速かつ再現性あるEDAを実現しよう』でした。
はじめに
個人開発や社内の小さな検証(PoC)では、まず「動くもの」を短時間で出したいことが多いです。 その場合に問題となるのが『マネージドDB高すぎ問題』です。
Webアプリケーション費用の大半がDB代なのではないかというぐらいDBは高いです。CloudSQLを最小スペックで立てたとしても$10/月ほどかかります。AI時代において、とりあえずvibe codingで概念だけ作ってみましょう、という戦略を取るにしても、デモアプリで作っておくだけにしては少々お高めです。個人開発では尚更でしょう。
かといってFirestoreやDynamoDBのようなNoSQLだとユースケースの変更に弱くなります。ちょっと作ってみよう、でユースケースを固め切るのもあまり現実的ではありません。
この記事では、Cloud Run × SQLite × Litestream という組み合わせで、RDBを採用しつつどこまで安く作れるのかを見ていきます。
個人開発ではSupabaseやNeon、TursoなどサーバレスなDB環境に頼る手もあります。一方で、企業内ではGCPやAWSで完結させた方が何かと都合よいため、今回はこの組み合わせを試してみます。
Litestreamとは
Litestream は SQLite の変更を追いかけて、GCSやS3などのオブジェクトストレージにレプリケートするツールです。 2023年にv0.3.13がリリースされてからしばらく開発が停滞していた時期がありましたが、2025年10月にv0.5.0がリリースされてから継続的な開発が再開されています。本記事ではv0.5.10を使用します。*1
SQLite は 記録の仕組みとして、WAL(Write-Ahead Logging)で変更を追記します。WALでは、トランザクションの開始・終了とデータの変更情報のログを先にディスクへ記録することで、データの一貫性を担保します。
Litestream はこの WAL を監視し、変更を LTX という形式にまとめて、設定したレプリカ先(今回の場合は gs://...)へアップロードします。このとき、GCSにはトランザクション単位のファイルが並びます。
このあたりの責務の分かれ方が綺麗なのですが、Litestream は あくまでSQLiteを利用するアプリケーションの外側から機能します。
- 起動時、GCS 側にレプリカがあれば
litestream restoreで DB を戻す - その DB ファイルを使ってアプリを起動する
- 実行中は
litestream replicate -execでアプリを子プロセスとして動かし、SQLite の変更を追い続ける
このようにすることで、アプリはSQLiteを使って普通にCRUDをするだけで、変更の追跡やGCSへのアップロードはLitestreamが担います。
アプリ側はLitestreamの存在を意識する必要がないため、SQLiteが使用できれば言語・フレームワークに制限はありません。
詳しい仕組みに関しては、Litestream How It Worksを参照してください。
作ってみた
この記事では次のような構成を作成しました。
デモアプリはタスクを登録・更新・削除できるアプリで、フロントエンドとバックエンドを持つ構成になっており、DBにSQLiteを使用します。 Terraformでインフラ設定やDockerイメージのビルドまで実行するので、設定を少し編集するだけで立ち上げることが可能です。
APIにRustを使用しているのは「RustでSQLiteを扱ったことないなあ」という筆者の興味からだけであって、SQLiteが利用できるアプリケーションであれば、どんな言語でも構いません。
- API
- Rust(axum)+ SQLite の CRUD
- API仕様そのものはhttps://zenn.dev/kou_pg_0131/articles/google-cloudrun-litestreamを参考にしています
- フロント
- Vite + React
- ビルド時に API のベース URL を埋め込み、nginx で静的配信
- 本番
- コンテナ内で Litestream が
replicate -execにより API を起動し、SQLite(WAL)を GCS のバケット配下にレプリケート
- コンテナ内で Litestream が
- IaC
- Terraform で Artifact Registry、GCS(Litestream 用)、Cloud Run v2(API / フロント)、必要 API の有効化
- 今回の構成ではDockerイメージのビルド&プッシュもTerraformのnull_resourceでトリガーされる
APIの構成
本構成で肝になるのはAPIの構成です。
まず、APIはRustで作成しており、DockerfileではRust製のAPIをビルドしつつ、Litestreamのバイナリと設定ファイル、起動用のスクリプトを同梱します。
# Rust ビルド FROM rust:1-bookworm AS builder WORKDIR /build COPY Cargo.toml Cargo.lock ./ COPY src ./src RUN cargo build --release # Litestream バイナリ(公式イメージ v0.5.10) FROM litestream/litestream:0.5.10 AS litestream FROM debian:bookworm-slim COPY --from=litestream /usr/local/bin/litestream /usr/local/bin/litestream COPY --from=builder /build/target/release/task_api /usr/local/bin/task_api COPY litestream.yml /etc/litestream.yml COPY run.sh /backend/run.sh RUN chmod +x /backend/run.sh && mkdir -p /backend/data ENV DATABASE_PATH=/backend/data/tasks.db EXPOSE 8080 CMD ["/backend/run.sh"]
litestream.ymlは次のようになっており、replicaを作成する場所をここで定義します。
dbs: - path: ${DATABASE_PATH} replica: url: gs://${LITESTREAM_REPLICA_BUCKET}/tasks
backendが起動するときは run.sh が実行されるようになっていますが、run.shは次のようになっています。 この中ではlitestreamのrestoreとreplicateが実行され、APIはlitestreamの子プロセスとして起動されます。
#!/usr/bin/env bash set -euo pipefail litestream restore -if-replica-exists -config /etc/litestream.yml "${DATABASE_PATH}" exec litestream replicate -exec "/usr/local/bin/task_api" -config /etc/litestream.yml
フロントエンドについてはAPIのCRUD操作をして画面に表示するだけなので、特に難しいことはなく本記事では説明を割愛します。
動かす手順
次の手順に則ることでタスク登録のデモアプリをCloud Run上にデプロイし、動かすことができます。
まずはGCPのプロジェクトを設定し、Terraform stateを保存するGCSバケットを作成します。
gcloud auth application-default login # 後でTerraformを実行するための認証情報もここで設定しておく gcloud config set project <YOUR_PROJECT_ID> gcloud storage buckets create gs://<YOUR_UNIQUE_STATE_BUCKET_NAME> \ --project=<YOUR_PROJECT_ID> \ --location=<YOUR_LOCATION>
次にTerraformの設定を行います。
いくつか自身のプロジェクトに合わせて設定を編集する部分があるため、 サンプルファイルをコピーして編集します。
cd terraform # プロジェクト設定に合わせて値を編集 cp terraform.tfvars.example terraform.tfvars # プロジェクト設定に合わせて値を編集 cp backend.hcl.example backend.hcl
次にTerraformを初期化し、インフラを作成します。
null_resourceを使用して、ローカルでDockerfileをビルドし、Artifact Registryへプッシュします。*2 Cloud Runのコンテナへの接続もこのタイミングで行われるため、applyが完了するとAPIとフロントエンドが利用できる状態です。
terraform init -backend-config=backend.hcl terraform apply
動作確認してみる
実際に動作を確認してみましょう。
まずはAPIを叩いてみます。正しく高速に動作していそうです。
❯ curl -sS "https://task-app-api-n5xxxxxxxx-an.a.run.app/tasks" \ -X POST \ -H 'Content-Type: application/json' \ --data '{ "title": "cloud run test" }' {"id":4,"title":"cloud run test","completed":false}%
実測すると50ms程度で応答していることが確認できます(Cloud Runがウォームスタートの場合)
curl -w"http_code: %{http_code}\ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" -sS "https://task-app-api-n5xxxxxxxx-an.a.run.app/tasks" http_code: 200 time_namelookup: 0.001935 time_connect: 0.015262 time_appconnect: 0.030953 time_pretransfer: 0.031061 time_starttransfer: 0.056591 time_total: 0.056628
フロントエンドで確認してみましょう。 こちらも1人で利用している分にはロードや更新で遅延を感じることは特にありません。

GCS上にもLTX形式のファイルが複数作成されていることが確認できます。

15分以上時間をおいてCloud Runのインスタンスが停止するまで待ち1、再度アプリケーションを開いてみてもデータが永続化されていることが確認できました。コールドスタートにより若干の遅延は感じますが、それでも大きくUXを損なうような遅延ではありませんでした。
注意事項
Litestream はローカルのSQLiteをリアルタイムでGCSへレプリケートできます。ただし、逆にGCSのデータをリアルタイムでローカルのSQLiteへ復元できません。したがって、複数のライターから同時にSQLiteへ書き込めず、単一ライターのみをサポートしています。
今回の構成においてもAPIの同時実行数は1に制限することで、同時に書き込みが発生しないようにしています。これはLitestreamの技術的制約であり、単一のインスタンスによる垂直スケール以外のリソース増強は現在のところできません。*3
また、データ量が多くなるとGCSからのダウンロードに時間がかかり、アプリの応答速度が低下することも考えられます。今回試したアプリケーション例では高速に動作しており、特に気になることはありませんでした。
まとめ
注意事項はありつつも、Cloud Run × SQLite × Litestream という構成はRDBの柔軟性を確保しつつ低コストに実現できます。PoCや個人開発においては十分実用的な選択肢です。 事業がスケールした際にはCloud SQLなどの別DBに載せ替えたり、SQLite互換のサービスであるTursoへの移行を検討してみるのもよいでしょう。
We are Hiring
弊社では爆速で動くアプリケーションをわいわいと作るエンジニアを募集しています。 興味がある方は次のリンクから応募をお待ちしています。