前回、素のterraform destroyでCognito User Poolごと消し飛ばし、ユーザー登録を全滅させた。復旧後にTerraformを認証基盤(infra/auth)と実行基盤(infra/service)の2スタックに分離したのだが、その反省会で「なぜ分けるのか」を言語化したら思いのほか汎用的な設計論になったので、書き残しておく。
軸1: 重要だから守るのではない。作り直せないから守る
最初は「ユーザー情報は重要だから隔離する」と考えかけた。これは半分しか合っていない。
ECSもALBもACM証明書も重要だ。落ちればサービスは止まる。しかしこれらは設計図(コード)から寸分違わず再生成できる。だから壊しても実は何も失わない。現に構築当日、再現性の検証と称して本番環境を2回壊して2回作り直した。壊せることは欠点ではなく、Infrastructure as Codeの成果そのものである。
一方、User Poolは違う。Terraformコードに書いてあるのは「こういう設定のプールがある」ことだけで、「誰が登録されているか」という状態はコードの外にある。壊したら戻らない。ユーザー全員に「もう一度登録してください」と頭を下げる羽目になる。
つまり分離の第一の基準は重要度ではなく再現可能性だ。
- インフラ = コードから再生成できるもの。git が真実。気軽に壊して再現性を検証すべき
- データ = 再生成できない状態を持つもの。破壊経路から隔離し、削除保護を付け、本気ならバックアップする
User Poolは後者である。インフラの顔をしたデータなのだ。
重要度が無意味なわけではない。効いてくるのは「保護の厚み」の判断だ。うちの会話履歴DB(EFS上のSQLite)も再現できないデータだが、今はデモデータなので、実行基盤と一緒に消えることを許容している。顧客の履歴を預かる日が来たら、同じ「再現できないデータ」でも扱いが変わる。置き場所は再現可能性で決め、保護の厚みは重要度で決める。
軸2: 壊したとき、誰が巻き添えになるか
分離にはもう1つの軸がある。依存の広がり(爆風半径)だ。
実行基盤に依存しているのは、そのサービス1つだけである。voice.pocraft.netの実行基盤を壊しても、被害はvoice.pocraft.netで完結する。
認証基盤は違う。User Poolは複数のサービスから参照され得る共有層だ。次のデモアプリを作るとき、同じプールを指せばユーザー登録を使い回せる——顧客は一度登録すれば、こちらの出すデモ全部にログインできる。フリーランスのデモ運用では、これは積極的に共有したい資産である。共有層を壊すと、将来の全サービスが巻き添えになる。
実はこの思想は構成のもう1箇所に無意識に現れていた。Route53のホストゾーン(pocraft.net)を、実行基盤のTerraformはresourceではなくdataで参照している。所有せず、レコードを1本足すだけ。ゾーンはブログなど他の用途とも共有される基盤だから、サービスのdestroyで消えてはならない——スタック分割と同じ判断を、こちらは最初から自然にやっていた。人は「明らかに共有のもの」には慎重になれるが、「1サービス目では専有に見えるもの」の共有性を見落とす。User Poolはまさにそれだった。
2軸で見ると、分割は二重に正当化される
| 軸 | 問い | 認証基盤 | 実行基盤 |
|---|---|---|---|
| 再現可能性 | 壊したらコードから戻るか | 戻らない(ユーザー登録という状態) | 戻る |
| 依存の広がり | 壊したら誰が巻き添えか | 将来の全サービス | そのサービスだけ |
両軸とも同じ答えを指している。認証=守る層、実行=使い捨て層。だからstateを分け、守る層にはdeletion_protection = "ACTIVE"を付け、使い捨て層はterraform destroy一発で消せるままにしておく。
なお実行スタックは認証スタックの出力(pool ID等)をterraform_remote_stateで読む。参照の向きは常に「使い捨て層 → 守る層」の一方向で、逆はない。守る層は下の事情を知らない。
最終形は3層
正直に言うと、今の構成には妥協が1つある。会話履歴(EFS)という「再現できないデータ」が、使い捨てのはずの実行基盤に同居していることだ。デモデータのうちは許容だが、預かりものになったら引っ越しが要る。
行き着く先はおそらくこうだ。
- 認証層 — User Pool。共有・状態あり。原則destroyしない
- データ層 — 履歴DBなど。専有だが状態あり。バックアップ付き
- 実行層 — ECS/ALB/証明書。ステートレス。壊し放題
教訓は一行にまとまる。壊してよいものと壊してはならないものを、同じstateに置くな。 それを見分けるのは重要度ではなく、「作り直せるか」と「誰が巻き添えか」の2つの問いである。
リポジトリ(公開しました): github.com/yoshiharu-ishii/realtime_voice
リアルタイム音声AI 開発シリーズ
- Claude Codeと1日で作るリアルタイム音声AI ― $5から始めて、RAG・ペルソナ・Cognito認証・Terraformまで
- 「コードは読まない」開発スタイル ― Mermaid図解とモジュール分割で人間とAIの分業を作る
- AIが空耳する日 — 音声通話アプリの誤検知対策、文字起こし崩壊の犯人探し、そしてコンテナ化
- terraform applyで音声AIサービスが立つまで — User Pool全消し事故と、死なないAPIキーの謎
- User Poolはインフラではなくデータである — Terraformスタック分割の2軸(この記事)
- WebSocket中継 vs WebRTC直結 ― 両方実装して固有名詞で殴り合わせたら、精度の勝敗とバグが2匹出てきた
- 無線機から電話へ ― サーバーVADでハンズフリー通話を実装する