個人で作っているSaaSが2つあって、どちらもLambda + Postgresの構成。最初はそれぞれにRDSを立てていたが、1台あたり月$20〜30かかる。売上がまだ立っていない検証段階でこれは重い。なのでRDSを1台に集約して、スキーマで分けることにした。今は db.t3.micro 1台に2プロダクトが同居している。
スキーマで分けて、越境は権限で禁止する
プロダクトごとに別スキーマを切って、接続時に search_path を固定する。アプリのDBユーザーには自分のスキーマしか見えないようにして、間違って隣のプロダクトのテーブルを触れないようにしておく。
-- プロダクトAの接続ユーザー
GRANT USAGE ON SCHEMA app_a TO app_a_user;
ALTER ROLE app_a_user SET search_path = app_a;
-- 他スキーマへの権限は渡さない
テーブル作成やマイグレーションのDDLは、アプリユーザーではなく管理者ユーザーで別に流す。アプリ側にDDL権限を持たせないので、実行時に隣を壊す事故が構造的に起きない。ここは「たぶん大丈夫」で済ませず、権限で閉じておきたかった。
Lambdaからの接続でハマったところ
Lambdaは実行のたびにコンテナが使い回されたり捨てられたりする。SQLAlchemyのプールをそのまま使うと、しばらく放置されたコネクションが向こうで切れていて、次のリクエストで死ぬ。pool_recycle を短め(300秒)にして、古いコネクションを掴まないようにした。
もう一つ、非同期処理で asyncio.run を挟んだあとにHTTPレスポンスを返すと、閉じたイベントループ上の接続でエラーになることがあった。これは接続まわりというより実装の順序の問題で、本番でしか出ず、特定に一番時間を使った。
インスタンスが小さいので、張れる接続数もそう多くない。プールサイズは控えめにして、同時実行の上限側でも絞っている。
コストの内訳
-
db.t3.micro(24時間起動): 約$28/月 - NATはGatewayを使わず
t3.nanoの自前NATインスタンス: 約$4/月 - Secrets Manager / KMS / Route53 など: 合わせて$5前後
2プロダクト合わせて月$40台に収まっている。NAT Gatewayを素直に使うとそれだけで$32/月かかるので、ここを自前にしているのが効いている。RDSを常時起動にしているのはwebhookをいつでも受ける必要があるからで、ここは削れない。
売上が立つまではこの構成で粘って、ダメなら丸ごと落とせば$0近くまで下がる。撤退が安いのは、検証段階だと地味に大事だと思っている。