できるかぎり AWS で Web アプリケーションを安く作れないかな、という試みの一環。アプリケーションの内容は特に重要ではなく、適当な Rails アプリです。
Litestream が Kaigi on Rails 2025 でも言及されていた ので、一回使ってみたくてやった、という内容です。
Litestream とは。SQLite の WAL を外部ストレージにストリーミングするツールです。概ねオブジェクトストレージ (今回は S3) へのレプリケーション (かつ PITR) が実現できると思えばヨサソウ。バイナリをポン置きで動くのも魅力。
背景と目的
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 にアクセスする構成にした。
アーキテクチャ概要
- Lambda 起動時に、S3 からリストアする
- アプリケーション起動前に Litestream でレプリ開始
- Rails アプリがリクエストを処理し、データを読み書きする
- SQLite への書き込みが発生したら、Litestream が随時 S3 にストリーミングする
- 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 起動時にやっちゃうかも……)
- Lambda の前段 (CloudFront) でメンテに入れる
- Lambda の
reserved_concurrent_executionsを一時的に 0 に設定。今来ているリクエストを処理しきるまで待つ reserved_concurrent_executionsを 1 に戻して、bin/rails-remote db:migrateやbin/rails-remote runner '<スクリプト>'を実行- メンテを明け、HTTP リクエストを受け付ける
コスト感
全体構成としては CloudFront + Lambda FunctionUrl + SQLite3 + Litestream + S3。
CloudFront も Lambda も S3 もほぼ無料なのでほぼ無料で運用できている。データが増えてきたときに restore 時間がどう延びていくかはここから眺めていこうかな。まぁいい構成じゃないかしら。
その他
Litestream v0.5.0
ついこのあいだ (2025-10-02) v0.5.0 が出て、v0.3 系から migrate する必要がありました。
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_options に host を設定して、redirect_to foo_path じゃなく foo_url を使うようにして処理した。
ここで Rails 8.1 (最近出ましたね!) にちょうどいい機能が入っているので、有効に使うと良いんじゃないかな。
感想
データストアを意識する必要がない、アプリケーションの Task だけ考えれば良いって環境、異常に気楽ですごい。これからは Solid シリーズが流行るだろう。
kinoppyd のトーク をぜひ聞いてみたい。