Lambda + SQLite3 + Litestream + S3 でデータを保持してみた

できるかぎり AWS で Web アプリケーションを安く作れないかな、という試みの一環。アプリケーションの内容は特に重要ではなく、適当な Rails アプリです。

Litestream が Kaigi on Rails 2025 でも言及されていた ので、一回使ってみたくてやった、という内容です。

Litestream とは。SQLite の WAL を外部ストレージにストリーミングするツールです。概ねオブジェクトストレージ (今回は S3) へのレプリケーション (かつ PITR) が実現できると思えばヨサソウ。バイナリをポン置きで動くのも魅力。

litestream.io

背景と目的

Rails (に限らないかな。SSR するアプリ) を提供しようと思うと、少なくともアプリケーションサーバとデータストアが必要になる。

アプリケーションサーバについては、Lambda を使うと無料枠もあり、非常に低コストで運用できる。 データストアは RDS を使うと確実だが、継続して立ち上げ続けることになるのでコストが跳ね上がる。SQLite をローカルで使うと無料になるし、SQL から離れることになるが DynamoDB を使うと無料枠があるので安く運用可能である。

つまり Lambda + SQLite で構成すると、最小コストで運用できるはず、となるが、Lambda は実行環境が消えるという問題がある。

Lambda や ECS のようなサーバーレスな環境だと、タスク終了時にローカルストレージも破棄される。このせいでタスクが変わるたびにデータが消えてしまう。

対策としては

  • Lambda + EFS マウント
  • Lambda + S3 + Litestream (今回の構成)

になるんじゃないかな。ローカルのデータをリアルタイムにリモートにも送っておけば、生まれ変わっても引き続き使える、という対策です。EFS もまぁやったら動くと思う。

SQLite にはもう一つ課題があって、Lambda をスケールアウトできない。EFS のように複数 Task からマウント可能でも、SQLite への同時書き込みでデータが壊れる (はず)。 今回は Lambda の同時実行数 (reserved_concurrent_executions) を 1 に設定することで、単一の Task のみが SQLite にアクセスする構成にした。

アーキテクチャ概要

  1. Lambda 起動時に、S3 からリストアする
  2. アプリケーション起動前に Litestream でレプリ開始
  3. Rails アプリがリクエストを処理し、データを読み書きする
  4. SQLite への書き込みが発生したら、Litestream が随時 S3 にストリーミングする
  5. Lambda が終了しても S3 上に最新の状態が残っている

Lambda のコールドスタート時に毎回 restore が走るので起動が遅くなるが、まぁそもそもコールドスタートだからな。

Rails からは普通に sqlite3 として使うだけ。Litestream でのリストアやレプリケーションは Docker の entrypoint でいい感じにやります。

# Restore database replica if available
if litestream restore -if-db-not-exists -if-replica-exists -config /rails/config/litestream.yml /tmp/storage/production.sqlite3; then
  echo "Database restored from S3"
fi

# Start Litestream replication && exec puma
litestream replicate -config /rails/config/litestream.yml \
  -exec 'puma -b "tcp://0.0.0.0:8080" -e production'

db:migrate

reserved_concurrent_executions = 1 で運用しているため、db:migrate が実行できない。まぁ 2 個立ち上がるとデータ壊れるので正しいんだけど。

なので、以下の手順で migrate を実行している。(大変めんどくさいので migrate は Lambda 起動時にやっちゃうかも……)

  1. Lambda の前段 (CloudFront) でメンテに入れる
  2. Lambda の reserved_concurrent_executions を一時的に 0 に設定。今来ているリクエストを処理しきるまで待つ
  3. reserved_concurrent_executions を 1 に戻して、bin/rails-remote db:migratebin/rails-remote runner '<スクリプト>' を実行
    • bin/rails-remote は Lambda の環境変数RAILS_COMMAND を設定して invoke するスクリプトです
    • entrypoint では、litestream restore 後 puma 起動前に、RAILS_COMMAND が設定されている場合は litestream replicate -execrails コマンドを実行して終了する
  4. メンテを明け、HTTP リクエストを受け付ける

コスト感

全体構成としては CloudFront + Lambda FunctionUrl + SQLite3 + Litestream + S3。

CloudFront も Lambda も S3 もほぼ無料なのでほぼ無料で運用できている。データが増えてきたときに restore 時間がどう延びていくかはここから眺めていこうかな。まぁいい構成じゃないかしら。

その他

Litestream v0.5.0

ついこのあいだ (2025-10-02) v0.5.0 が出て、v0.3 系から migrate する必要がありました。

fly.io

litestream gem

litestream gem については特に使っていません。起動は Docker の entrypoint で管理していて苦労していないし、プロセスの親子関係としては Litestream が親の方がデータが正しくなりそうだし、これでいいかなーと思って。

非同期処理

ActiveJob は今のところ使ってないんだけど、使うとしたら solid queue の puma plugin でやるんだと思う。

バックアップ

まだ数日しか経ってないのでちゃんと見てないけど、S3 に snapshot が残っていそうなので、これで運用できるんじゃない……か?

redirect_to

CloudFront の裏に FunctionUrl で Lambda を置いていると、Host ヘッダが FunctionUrl のものになる。 redirect 時に CloudFront のドメインじゃなく FunctionUrl のドメインに飛んでしまって困った。

config.action_controller.default_url_optionshost を設定して、redirect_to foo_path じゃなく foo_url を使うようにして処理した。

ここで Rails 8.1 (最近出ましたね!) にちょうどいい機能が入っているので、有効に使うと良いんじゃないかな。

Allow hosts redirects from `hosts` Rails configuration by byroot · Pull Request #55420 · rails/rails · GitHub

感想

データストアを意識する必要がない、アプリケーションの Task だけ考えれば良いって環境、異常に気楽ですごい。これからは Solid シリーズが流行るだろう。

kinoppyd のトーク をぜひ聞いてみたい。

tech.smarthr.jp

参考URL