安く作りたい、でもRDBは欲しい。Cloud Run × SQLite × Litestream を試す。 - エムスリーテックブログ

エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

安く作りたい、でもRDBは欲しい。Cloud Run × SQLite × Litestream を試す。

AI・機械学習チームの中村伊吹(@inakam00)です。 この記事はAI・機械学習チームブログリレー6日目の記事です。

最近はNetflixでコナンの映画が配信されるようになり、毎日コナンの映画を1つずつ消化するのが日課になっています。 『探偵たちの鎮魂歌』はギミックがわかりやすくてお気に入りです。

前日は高橋さんの『Agentic Coding時代のデータ分析環境: marimo + gokartで高速かつ再現性あるEDAを実現しよう』でした。

www.m3tech.blog

はじめに

個人開発や社内の小さな検証(PoC)では、まず「動くもの」を短時間で出したいことが多いです。 その場合に問題となるのが『マネージドDB高すぎ問題』です。

Webアプリケーション費用の大半がDB代なのではないかというぐらいDBは高いです。CloudSQLを最小スペックで立てたとしても$10/月ほどかかります。AI時代において、とりあえずvibe codingで概念だけ作ってみましょう、という戦略を取るにしても、デモアプリで作っておくだけにしては少々お高めです。個人開発では尚更でしょう。

かといってFirestoreやDynamoDBのようなNoSQLだとユースケースの変更に弱くなります。ちょっと作ってみよう、でユースケースを固め切るのもあまり現実的ではありません。

この記事では、Cloud Run × SQLite × Litestream という組み合わせで、RDBを採用しつつどこまで安く作れるのかを見ていきます。

個人開発ではSupabaseNeonTursoなどサーバレスなDB環境に頼る手もあります。一方で、企業内ではGCPやAWSで完結させた方が何かと都合よいため、今回はこの組み合わせを試してみます。

Litestreamとは

litestream.io

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を利用するアプリケーションの外側から機能します。

  1. 起動時、GCS 側にレプリカがあれば litestream restore で DB を戻す
  2. その DB ファイルを使ってアプリを起動する
  3. 実行中は litestream replicate -exec でアプリを子プロセスとして動かし、SQLite の変更を追い続ける

このようにすることで、アプリはSQLiteを使って普通にCRUDをするだけで、変更の追跡やGCSへのアップロードはLitestreamが担います。

アプリ側はLitestreamの存在を意識する必要がないため、SQLiteが使用できれば言語・フレームワークに制限はありません。

詳しい仕組みに関しては、Litestream How It Worksを参照してください。

作ってみた

この記事では次のような構成を作成しました。

デモアプリはタスクを登録・更新・削除できるアプリで、フロントエンドとバックエンドを持つ構成になっており、DBにSQLiteを使用します。 Terraformでインフラ設定やDockerイメージのビルドまで実行するので、設定を少し編集するだけで立ち上げることが可能です。

APIにRustを使用しているのは「RustでSQLiteを扱ったことないなあ」という筆者の興味からだけであって、SQLiteが利用できるアプリケーションであれば、どんな言語でも構いません。

github.com

  • API
  • フロント
    • Vite + React
    • ビルド時に API のベース URL を埋め込み、nginx で静的配信
  • 本番
    • コンテナ内で Litestream が replicate -exec により API を起動し、SQLite(WAL)を GCS のバケット配下にレプリケート
  • 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形式のファイルが複数作成されていることが確認できます。

トランザクションごとに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への移行を検討してみるのもよいでしょう。

turso.tech

We are Hiring

弊社では爆速で動くアプリケーションをわいわいと作るエンジニアを募集しています。 興味がある方は次のリンクから応募をお待ちしています。

エンジニア採用ページはこちら

jobs.m3.com

カジュアル面談もお気軽にどうぞ

jobs.m3.com

インターンも常時募集しています

open.talentio.com

*1:v0.3.x時代の記事が多く、v0.5.xで試している記事は多くありません(が、そんなに使用感は変わりません)

*2:Cloud Buildを使う方法もありますが、今回は手軽に実行するために手元でdocker buildを実行しつつ、操作を terraform apply だけで完結させることを優先しました。

*3:v0.5.9から1秒ごとのpolling updateが実装されているため、読み取りだけであればスケールアウトさせることが可能です。