第5章 インフラの信頼性・シークレット管理・障害対応
学習目標
- デュアルキー(Dual-Key / Overlapping Validity)受容構造を通じて、JWT署名シークレット、Webhookシークレット、APIキー、TLS証明書をサービス停止なしにローテーション(Rotation)し、キー流出時に緊急交換する手順を策定する。
- 定期スナップショットバックアップとWALベースのポイントインタイムリカバリ(Point-in-Time Recovery, PITR)を目標復旧時点(RPO)と目標復旧時間(RTO)の観点から比較し、定期復旧リハーサルによってバックアップの有効性を検証する。
- ローリングデプロイ環境においてアプリケーションとデータベーススキーマが共存できるよう、Expand-Contract(並行変更、Parallel Change)による3段階の無停止マイグレーションを設計し、破壊的なDDLロックを回避する。
- OpenTelemetryベースのログ・メトリクス・分散トレース(
trace_id)を用いてエッジからDB・LLMまでのボトルネックを追跡し、収集パイプラインで機密情報とシークレット値をマスキング(Scrubbing)する。 - クラウド・AIインフラのコストおよび使用量の異常兆候アラート(ソフト上限)と自動遮断上限(ハード上限)、サーキットブレーカーを組み合わせ、連鎖障害とコスト急増を防ぐ。
- 障害発生時に「原因分析よりも被害緩和と隔離が先」という原則のもと、フィーチャーフラグ・トラフィックロールバックのランブックを実行し、非難のない事後振り返り(Blameless Postmortem)を作成する。
はじめに
インフラエンジニアの間で語り継がれている格言があります。「すべてのシステムはいずれ壊れ、すべての秘密鍵はいつか交換しなければならず、テストしていないバックアップはバックアップではない。」
第1の試練は、**シークレット値のローテーション(Secret Rotation)**です。セキュリティポリシー上、四半期ごとにセッション署名シークレットやWebhook検証キーを変えなければならないのですが、新しいキーで環境変数を上書きしてデプロイしたところ、すでにログインしていたユーザー数万人が一斉にログアウトさせられたり、デプロイが進行する3分間に半分のWebhookが検証失敗(401)で消失したりします。さらに緊急度が高いのは、開発者のミスでAPIキーが公開リポジトリへ露出し、10分以内にキーを失効させなければならない場面です。
第2の試練は、データベース事故とスキーマ変更です。午後2時17分にデプロイされたバッチスクリプトのバグがWHERE句なしでUPDATEを実行し、顧客ランクのデータをすべて上書きしてしまいました。昨日の午前0時に取得しておいた日次スナップショットへ戻せば、今日の午前から午後2時16分までに積み上がった14時間分の決済と試験受験の記録が吹き飛んでしまいます。また、無停止デプロイ中にカラム名を変えようとALTER TABLE RENAME COLUMNを実行した瞬間、まだ旧バージョンのコードを動かしているサーバーインスタンス群が一斉にcolumn does not existエラーを吐き出して倒れていきます。
この章では、サービスをただの1秒も止めることなく秘密鍵とデータベーススキーマを安全に変える技術、いかなるデータ損失事故からも望みの時刻(秒単位)へと蘇らせるPITRバックアップ設計、分散環境のブラックボックスを開ける可観測性(Observability)とログマスキング、そして午前3時にアラートが鳴り響いたときに慌てることなく5分以内に出血を止める障害対応ランブックを学びます。
重要な概念
無停止でのシークレット値・証明書ローテーション:デュアルキー(Dual-Key)受容パターン
セッションCookieの署名キー(SESSION_SECRET)、内部サービス間の認証トークン、外部決済Webhookの署名キー、データベースアカウントのパスワードを変えるとき、なぜサービス停止や認証失敗が起きるのでしょうか。
それは、従来型のシングルキー(Single-Key)構造では、コードにキーがたった1つ(SECRET_KEY)しか入っていないからです。
- 午前10時00分に発行されたユーザーのセッションCookieは、旧バージョンのキー(
Key_V1)で署名されてブラウザに保存されています。 - 午前10時10分に管理者がサーバーの環境変数を新しいキー(
Key_V2)へ変更し、ローリングデプロイを開始します。 - デプロイが進行している間、一部のサーバーインスタンスはまだ
Key_V1を使い、一部はKey_V2を使います。 - デプロイが終わったあとも、ユーザーが持ってくる
Key_V1署名のCookieはKey_V2しか知らない新サーバーですべて「偽造された署名」として拒否され、全ユーザーが強制ログアウトさせられます!外部Webhookも、送信者側でキーを変えるタイミングと受信者(私たちのサーバー)が再デプロイされるタイミングを0.001秒単位で同時に合わせることはできないため、必ずリクエスト消失の区間が生じます。
この問題を解決する標準アーキテクチャが、まさにデュアルキー受容(Dual-Key Acceptance、または複数キースロットCURRENT + PREVIOUS)パターンです。核心となるアイデアは、**「署名(発行)は常に最新のキー1つだけで行いつつ、検証(受容)は最新キーと直前の旧バージョンキーのリストをすべて許可する猶予期間(Grace Period)を設ける」**というものです。
// 無停止ローテーションをサポートするデュアルキー(キーリング)検証構造の例
// SESSION_SECRETS="v2_new_secret_key,v1_old_secret_key" の形式で注入
const keyRing = (process.env.SESSION_SECRETS ?? "")
.split(",")
.map((k) => k.trim())
.filter(Boolean);
export function signToken(payload: string): string {
// 新しく発行するトークンは、常に1番目のキー(最新キー、keyRing[0])でのみ署名する。
return hmacSign(payload, keyRing[0]);
}
export function verifyToken(token: string): boolean {
// 検証するときは最新キーから試し、失敗したら直前のキー (keyRing[1]) でも検証を試みる。
return keyRing.some((secret) => hmacVerify(token, secret));
}
この構造を備えておけば、どのような秘密鍵であっても次の4段階の無停止ローテーション手順によって、ただの1件の失敗もなく交換することができます。
| 段階 | 実行作業 | 署名(発行)キー | 検証(許可)キーリスト | ユーザー・Webhookへの影響 |
|---|---|---|---|---|
| 第1段階(準備・並行受容) | 新キー(V2)を生成し、検証許可リストへ追加してデプロイ |
V1(またはV2) |
[V2, V1]の両方を許可 |
停止0件(V1とV2のどちらが来てもすべて通過) |
| 第2段階(送信の切り替え) | 新トークンの発行および外部Webhook送信者の設定をV2へ切り替え |
V2 |
[V2, V1]の両方を許可 |
新規リクエストはV2で署名され、既存のV1セッションも猶予期間中は正常通過 |
| 第3段階(猶予および観測) | 既存トークンの有効期限(TTL)の間待機し、ログでV1の一致件数が0へ減るか確認 |
V2 |
[V2, V1] |
V1使用量の0件への収束を確認 |
| 第4段階(旧キーの失効) | 検証リストからV1を削除し、クラウド/ベンダー側でV1を永久失効(Revoke) |
V2 |
[V2]単独 |
ローテーション完了(流出リスクの排除) |
データベースのパスワードを無停止でローテーションするときも原理はまったく同じです。同じ権限を持つ2つのDBユーザー(app_user_blue、app_user_green)を交互に使う交代ユーザー(Alternating Users)戦略を採れば、現在アプリがblueアカウントで稼働している間にgreenアカウントのパスワードを新しく変更し、アプリの接続文字列をgreenにしてデプロイし、以前の接続がすべて抜けたあとにblueアカウントをロックできるため、1秒の接続エラーも発生しません。
キー露出事故時における定期ローテーションと緊急失効(Emergency Revocation)の判断基準
では、もし開発者が誤って.envファイルを公開GitHubリポジトリへプッシュしてしまったり、攻撃者にキーが奪取されたことが確認されたりした**非常事態(Active Compromise)**であっても、上記のように悠長に24時間の猶予期間を設けてローテーションすべきでしょうか。
状況によって判断が異なります。
- 私たちが直接発行者・消費者を統制する外部APIキー(例:OpenAI APIキー、AWS IAMキー、Supabase
service_roleキー):- まず新キー(
V2)を即座に発行して私たちの本番環境変数へデプロイし、正常なサービストラフィックを1〜2分以内にV2へ移した直後、猶予期間を置かずに即座に旧キー(V1)をコンソールから永久失効(Revoke/Delete)します。もし攻撃者がいまこの瞬間にも流出したV1キーでデータを抜き出していたり、数百万円のAPI料金を発生させていたりするなら、新キーのデプロイよりも旧キーの即時失効・遮断が優先されます(一時的なエラーの方が、全データの流出よりはるかにましだからです)。
- まず新キー(
- ユーザーセッション署名キー(
SESSION_SECRET)が流出した場合:- 攻撃者が任意の管理者セッションCookieを自分で偽造して作り出せる状態にあるため、
V1を検証リストに残しておけば攻撃者の偽造Cookieも通過し続けてしまいます。したがってこの場合は、定期ローテーションとは異なりV1を即座に検証リストから削除してすべての既存セッションを強制失効させ、ユーザーに再ログインしてもらうようにしなければ安全を確保できません。
- 攻撃者が任意の管理者セッションCookieを自分で偽造して作り出せる状態にあるため、
バックアップ戦略とポイントインタイムリカバリ(PITR):RPO・RTOの設計と復旧リハーサル
インフラの信頼性を評価するときに最初に定義する、2つの時間指標があります。
- RPO(Recovery Point Objective、目標復旧時点):「最大でどれくらいの過去データの消失まで許容できるか?」(時間の量)。たとえばRPOが24時間であれば、昨日のバックアップ以降に今日積み上がった24時間分のデータが消えても許容されるという意味であり、RPOが1分であれば、事故直前の1分以内の状態までデータを救い出さなければならないという意味です。
- RTO(Recovery Time Objective、目標復旧時間):「障害が発生した瞬間からサービスを再び正常稼働させるまでに、最大で何分/何時間以内に完了させなければならないか?」(復旧所要時間)。
データベースのバックアップ方式は、大きく**定期スナップショット(Periodic Snapshot / Dump)とポイントインタイムリカバリ(Point-in-Time Recovery, PITR)**に分かれ、この2つが保証するRPOには天と地ほどの差があります。
| 区分 | 日次定期スナップショット (Daily Snapshot) | ポイントインタイムリカバリ (Point-in-Time Recovery, PITR) |
|---|---|---|
| 動作原理 | 1日1回(例:午前03:00)にディスク全体または論理ダンプを取得して保管 | ベース物理バックアップ(Base Backup)の上に、変更が起きるたび先行書き込みログ(WAL、Write-Ahead Log)を継続アーカイブ |
| 達成可能なRPO | 最大24時間のデータ消失(午前3時のバックアップ後に午後2時で事故が起きれば11時間分が蒸発) | 数秒〜数分以内(秒単位の時刻指定が可能) |
致命的ミス(誤ってDROP/DELETEを実行)からの復旧 |
その日1日の間に起きたすべての正常取引まで諦めなければならない | ミスクエリが実行された14:17:05の直前である**14:17:04時点**までWALを再生し、データ損失なしで復元可能 |
したがって、決済、認定証の発行、顧客文書のようにデータ消失を許容できない本番データベースでは、必ず**PITR(継続的WALアーカイブ)**を有効にしなければなりません。
しかし、バックアップを有効にしておくだけでは半分しか終わっていません。実務では、いざ障害が起きてバックアップを復元しようとしたときに、(1)バックアップファイルが破損していたり、(2)暗号化キー(KMS)の権限がなかったり、(3)復元するだけで8時間かかってRTOを大幅に超えてしまったり、(4)データベースは復元できたもののStorageバケットのファイル群と整合性が合わなかったりすることが日常茶飯事で起きます。そのため、四半期ごとに**本番環境から隔離された新しいインスタンスへ実際にPITRバックアップを復元して整合性クエリを回し、所要時間(実際のRTO)を測定する定期復旧リハーサル(Restore Drill)**を義務づけなければなりません。
無停止スキーママイグレーション:Expand-Contract(並行変更)パターン
クラウドサービスは、デプロイするときであっても止まることなく動き続けます(ローリングデプロイまたはカナリアデプロイ)。つまり、新バージョンのアプリケーションコード(App v2)が立ち上がる数分間も、旧バージョンのアプリケーションコード(App v1)が同時に生きており、**同じデータベースを一緒に読み書きしています。**また、もしApp v2でバグが発見され、10分後に再びApp v1へ即座にロールバックしなければならなくなることもあります。
この事実を忘れ、データベースのカラム名をfull_nameからdisplay_nameに変えようとして、次のようなマイグレーションを一度に実行したらどうなるでしょうか。
-- 絶対禁止!(即座のサービス障害とロールバック不能を招く破壊的な単一段階マイグレーション)
ALTER TABLE users RENAME COLUMN full_name TO display_name;
このSQLが実行された瞬間、まだ生きているApp v1のインスタンス群がSELECT full_name FROM usersを投げるたびに、すべて500 Internal Server Errorを出して倒れてしまいます!仮にApp v2へすべて切り替わったとしても、App v2で別のバグが出てロールバックボタンを押せば、App v1はすでに変わったスキーマと互換性がないため、ロールバックすら不可能な袋小路に閉じ込められます。
これを解決する唯一の定石技法が、まさに**Expand-Contract(拡張・収縮、または並行変更 Parallel Change)**パターンです。破壊的な変更を一度に行うのではなく、後方互換性(Backward Compatibility)を守る3〜4段階の段階的デプロイに分割して進めます。
| 段階 | データベースの変更 | アプリケーションのデプロイ | ロールバック可否および安全性 |
|---|---|---|---|
| 第1段階:拡張 (Expand) | 既存のfull_nameはそのまま残し、新カラムdisplay_nameをNULL許容(Nullable)で追加(ADD COLUMN display_name text) |
App v1がそのまま動く(full_nameのみを読み書き) |
full_nameがそのままあるためApp v1は100%正常動作 |
| 第2段階:二重書き込みおよびバックフィル (Dual-Write & Backfill) | 過去の行のfull_name値をバッチ(小規模チャンク単位)でdisplay_nameへコピー(Backfill) |
App v2デプロイ:データを書き込むときはfull_nameとdisplay_nameの**両方に記録(Dual-Write)**し、読み取るときはdisplay_name(なければfull_name)を読む |
いつでもApp v1へ即座にロールバックしても、full_nameに最新データが溜まり続けているためデータ消失0件! |
| 第3段階:切り替え完了 (Cutover) | すべての行が埋まったことを確認したあと、制約条件を検証(NOT NULL VALIDATE) |
App v3デプロイ:ここでコードからfull_nameの参照を完全に消し、display_nameのみを読み書きする |
すべてのインスタンスがもはや旧カラムを参照しない |
| 第4段階:収縮 (Contract) | すべてのApp v3の安定化を確認したあと、ここでようやく旧カラムfull_nameを削除(DROP COLUMN full_name) |
App v3を正常運用 |
無停止スキーマ変更を完遂 |
PostgreSQLで大容量テーブルに対してDDLを実行するときは、**テーブルロック(Lock)**にもあわせて注意しなければなりません。
- 通常の
CREATE INDEXはインデックスを作り終えるまでテーブルへの書き込み(INSERT/UPDATE/DELETE)を丸ごと塞いでしまうため、稼働中は必ず書き込みを止めない**CREATE INDEX CONCURRENTLY**を使用しなければなりません(ただし、CONCURRENTLYはトランザクションブロックBEGIN ... COMMITの中では実行できないため、マイグレーションツールでトランザクションラッピングをオフにして実行します)。 - 新しい外部キーやチェック制約条件を追加するときも、テーブル全体をスキャンしながらロックする代わりに、まず
ADD CONSTRAINT ... NOT VALID(既存行の検査を省略し即座に完了)を実行したあと、VALIDATE CONSTRAINT(書き込みを止めない弱いロックでバックグラウンド検証)に分けて実行し、すべてのマイグレーションセッションにはSET lock_timeout = '3s'を設定して、万が一ロック待機が長引いた場合はサービス全体が止まる(Lock Queue Pile-up)前にマイグレーション自身が先に安全に失敗するよう設計します。
可観測性(Observability)の3本柱とOpenTelemetry分散トレーシング
障害が起きたとき、「なんだかどこか遅い気がします」という報告と、「ソウルリージョンの関数から始まったtrace_id=4bf92f...のリクエストがSupavisorを経てDBクエリは4msで終わったものの、外部LLM APIのスパンで28.4秒間止まっていた」という報告とでは、天と地ほどの差があります。システムの内部状態を外側から把握する能力である**可観測性(Observability)**は、3本の柱から成り立っています。
| 可観測性の3本柱 | 答える問い | インフラ・AI運用における代表指標 |
|---|---|---|
| メトリクス (Metrics) | 「いま症状が出ているか?どれくらい悪いか?」(数値の時系列集計) | リクエスト率(RPS)、エラー率(5xx/429比率)、遅延時間のパーセンタイル(p50, p95, p99)、DBアクティブ接続数、時間あたりトークン消費量 |
| ログ (Logs) | 「その瞬間に何の出来事が起きたのか?」(構造化された個別イベントJSON) | リクエストパス、ステータスコード、エラースタック、フォールバック発動事由、監査イベント(必ず検索可能なJSON構造化ログを使用) |
| トレース (Traces) | 「1つのリクエストがどこで時間を使い果たしたのか?」(区間別スパンSpanの連結) | W3C traceparent / OpenTelemetry trace_idで束ねられた**[エッジ → アプリ関数 → DBクエリ → AI Gateway → LLM呼び出し]**のウォーターフォール |
ここで平均遅延時間(Average Latency)だけを見るのは非常に危険です。99回のキャッシュヒットリクエストが10msで終わり、1回のAI推論リクエストに20,000ms(20秒)かかったとすると、平均は210msと極めて優秀に見えますが、実際のユーザーは深刻な遅延を経験しているからです。したがって、インフラのSLO(サービスレベル目標)とアラートは、必ずテールレイテンシ(Tail Latency)であるp95、p99を基準にしなければなりません。
ログスクラビング(Log Scrubbing)と機密情報のマスキング
可観測性パイプラインを構築する際に絶対に破ってはならないセキュリティの鉄則が、**ログスクラビング(Log Scrubbing / Redaction、機密情報の伏せ字化)**です。
デバッグを楽にしようとして、HTTPリクエストの全ヘッダー(headers)やリクエストボディ(body)、環境変数をそのままconsole.logやOpenTelemetryのスパン属性にダンプしてしまうと、次の値が外部のログ収集サービス(Datadog、Sentry、CloudWatchなど)に平文のまま永久保存されてしまいます。
Authorization: Bearer ...ヘッダーおよびセッションCookie(Cookie、Set-Cookie)- APIキー(
sk-...)、データベース接続文字列(postgresql://user:password@...) - ユーザーのパスワード、住民登録番号(マイナンバー等)、クレジットカード番号、個人の機密プロンプト原文
したがって、アプリケーションロガーとOpenTelemetry Collectorの段階で、password、secret、token、authorization、cookie、api_keyといったキーやパターンを自動的に[REDACTED]へ置換するスクラバー(Scrubber)を強制し、ユーザーの識別が必要なときはメールアドレスの原文の代わりに非可逆ハッシュ識別子とtrace_idを残さなければなりません。
コスト・使用量の異常兆候アラートとサーキットブレーカー(Circuit Breaker)
クラウドとAI APIは、使った分だけお金を払う従量課金制(Pay-as-you-go)です。従来型の物理サーバーであればトラフィックが殺到したらサーバーがダウンして終わりでしたが、オートスケーリングが有効になったクラウド関数とLLM APIでは、コードに無限リトライのバグがあったりウォレット枯渇攻撃(Denial of Wallet)が入ってきたりすると、一晩で数百万円の請求爆弾となって跳ね返ってきます。
したがって、コストとリソースの統制は**通知(Soft Limit)と強制遮断(Hard Limit)**の二重の安全装置として設計しなければなりません。
- 異常兆候アラート(Anomaly Detection & Budget Alerts):日次予想コストの50%、80%、100%の閾値だけでなく、普段の時間あたり平均消費速度の3倍以上に急増したときに、即座にオンコール(On-call)チャンネルへアラートを送ります。
- 予算上限とレート制限(Hard Caps & Quotas):午前3時には誰もメールのアラートに気づけないかもしれません。そのため、プロバイダー・AI Gateway・アプリケーションの各レベルでユーザーあたり/組織あたりの日次トークン上限と同時実行上限をコードで強制し、上限を超えたら自動的にリクエストを拒否(
429)するか、低コストな軽量モデルへ降格(Graceful Degradation)させます。 - 指数バックオフ(Exponential Backoff with Jitter)とサーキットブレーカー(Circuit Breaker):下位のDBや外部LLM APIが過負荷で失敗しているのに、すべてのサーバーインスタンスが0.1秒ごとに一斉にリトライの集中砲火を浴びせると(Thundering Herd / Retry Storm)、少し揺らいでいただけのシステムが完全に崩壊します。リトライは必ず待機時間を1秒 → 2秒 → 4秒と延ばしながらランダムなジッター(Jitter、揺らぎ)を混ぜて分散させ、連続失敗率が閾値を超えたらサーキットブレーカーを開いて(Open)一定時間は下位への呼び出しを即座に遮断し、代替応答(Fallback)を返すことで、下位システムが一息ついて復旧する時間を与えなければなりません。
障害隔離・ロールバックのランブック(Runbook)と非難のない事後振り返り(Blameless Postmortem)
実際の本番障害が発生したときに最も致命的なミスは、「いったいなぜこのバグが出たのかコードを1行ずつ調べて原因をすべて解明してから直して再デプロイしよう」と言って、1時間ものあいだ障害状態を放置することです!
インシデント対応の第一原則は、常に**「緩和が最優先(Mitigate First, Investigate Later)——まず出血を止める」**です。
標準的な障害対応ランブックは、次の5段階の順序で動きます。
- 検知および深刻度の分類(Detect & Triage):SLO違反アラート(例:5分間の5xxエラー率が5%を超過)が鳴ったら、直ちにインシデントコマンダー(Incident Commander)を指名し、影響範囲(全面停止のP1 vs 一部機能遅延のP2)を判別します。
- 即座の隔離および緩和(Isolate & Mitigate —— 5分以内を目標):
- 直前にデプロイしたコードが原因である可能性がわずかでもあるなら、コードを直して新しくビルドしようとするのではなく、**即座に直前の正常なデプロイ版へロールバック(Vercel Instant Rollbackなど)**します。
- 特定の新機能や外部連携が原因であれば、**フィーチャーフラグ(Feature Flag / Kill Switch)**をオフにして問題の機能だけを隔離し、中核サービスは生かしておきます。
- 外部LLMプロバイダーの障害であればAI Gatewayのフォールバックを強制有効化し、攻撃トラフィックであればWAFの遮断ルールやRate Limitを引き上げます。
- 状況の共有(Communicate):社内のステークホルダーと顧客向けステータスページ(Status Page)に、現在の症状と緩和措置の状況を透明性をもって共有します。
- 根本原因の分析および検証後の復旧(Resolve & Verify):出血を止めたあとにログとトレース(
trace_id)、メトリクスを分析して根本原因を修正し、回帰テストを通過させたうえで通常モードへ復旧します。 - 非難のない事後振り返り(Blameless Postmortem):障害が終わったあとに「誰がミスをしたのか(個人の責任)」を追及すると、次回からエンジニアは障害やミスを隠すようになります。その代わりに、**「どのようなシステム的・構造的な抜け穴が、そのミスを本番障害にまで至らせてしまったのか?」を問い、時間帯別のタイムライン、5 Whys(5回のなぜ)による根本原因、そして具体的な担当者と期限が記された再発防止アクションアイテム(例:CIへの破壊的マイグレーションリンターの追加、サーキットブレーカーの導入)**を残し、システムを一段と堅牢にします。
さらに深く
大規模PostgreSQL運用におけるゼロロック(Zero-Lock)DDLチェックリスト
第5章で学んだExpand-Contractパターンを実際に実行するときでも、PostgreSQLの内部ロックメカニズムを知らないと、「単にカラムを追加したり制約条件をかけたりするだけだと思っていたのに、サイトが3分間止まってしまった」という事故に遭うことがあります。
なぜでしょうか。PostgreSQLにおいてALTER TABLEは、コマンドの種類によってはACCESS EXCLUSIVEロック(読み取りであるSELECTまで含め、該当テーブルに対するすべてのクエリを遮断する最高レベルのロック)を要求するからです。
たとえALTER TABLE ADD COLUMN new_col text;自体はメタデータを書き換えるだけで1ミリ秒で終わるとしても、もしその瞬間に該当テーブル上で50秒かかる遅い分析クエリがすでに回っていたとしたらどうなるでしょうか。
- 遅いクエリがテーブルに対して弱い読み取りロック(
ACCESS SHARE)を握っています。 - 私たちが実行した
ALTER TABLEは、ACCESS EXCLUSIVEロックを得るために、遅いクエリが終わるまでロック待ち行列(Lock Queue)に並びます。 - 本当の惨事はここで起きます!その後ろから入ってくる数千件の一般ユーザーの
SELECTやINSERTクエリは、本来なら遅いクエリと一緒に実行できたはずなのに、前の列に並んでACCESS EXCLUSIVEを待っているALTER TABLEの後ろへすべて並ばされてブロッキングされてしまうのです!結果として、1ミリ秒で終わるはずのALTER TABLE1つが遅いクエリの後ろに並んだせいで、50秒間にわたってサービス全体のすべての照会・書き込みが止まってしまいます。
したがって、本番環境のPostgreSQLマイグレーションスクリプトには、必ず次の安全規則をコードとして組み込まなければなりません。
-- 1) ロック待機時間を短く制限し、もし3秒以内にロックを取得できなければ
-- サービスクエリを塞ぐことなくマイグレーション自身が即座に失敗してリトライするようにする。
SET lock_timeout = '3s';
SET statement_timeout = '30s';
-- 2) 新しい外部キーやチェック制約条件は、まず NOT VALID で即座に(メタデータのみ)設定し、
ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount >= 0) NOT VALID;
-- 3) その後、別の段階で VALIDATE CONSTRAINT により読み書きを止めずに検証する。
ALTER TABLE orders
VALIDATE CONSTRAINT orders_amount_positive;
マルチリージョン・マルチベンダーAIサービスにおける階層別ヘルスチェックとグレースフルデグラデーション(Graceful Degradation)
外部依存先が多いAIサービスにおいて、「付加機能1つの障害がサービス全体の障害へと波及しないように(Bulkhead、隔壁パターン)」設計することは、信頼性エンジニアリングの核心です。
たとえば、ユーザーが認定試験を受験する中核経路と、試験が終わったあとにAIが誤答学習ガイドを生成してくれる付加経路が同じサーバーにあるとしましょう。もし外部のLLM APIが全面停止したとき、サーバー全体のヘルスチェック(/api/health)がLLM APIの生存有無までひとまとめにして503を返すように作られていたらどうなるでしょうか。ロードバランサーは「私たちのサーバーが死んだ!」と判断し、試験の受験やログイン、成績表の照会までを含むサイト全体を落としてしまいます。
したがって、依存先は必ず**中核経路(Critical Path / Hard Dependency)と非中核経路(Non-critical Path / Soft Dependency)**に分離しなければなりません。
- ロードバランサーやDNSが参照する**生存ヘルスチェック(Liveness / Readiness Check)**は、アプリケーションプロセスとプライマリデータベースのように、そのサーバーが正常なトラフィックを受け付けるための必須条件のみを検査します。
- 外部LLM API、推薦エンジン、メール送信機といった非中核・外部の依存先で障害が検知されたら、サーバーを殺す代わりに**グレースフルデグラデーション(Graceful Degradation、段階的機能縮小・縮退運転)**モードへと切り替えます。すなわち、中核である試験の受験と採点は100%正常に提供し、AI解説の領域だけを「現在AI解説の生成が遅延しているため、しばらく後に通知でお送りします(Queuesへ格納)」という案内に置き換えてユーザー体験を守ります。
実務の場面
オンビットペイ:ただの1人もログアウトさせずに完遂した署名キーローテーションと流出APIキーの緊急失効
AIベースの企業経費精算サービスを運用する「オンビットペイ」は、セキュリティ監査の準備中に2つの課題へ同時に直面した。1つは、1年間一度も変えていなかったユーザーセッションJWT署名シークレット(AUTH_SECRET)とカード会社Webhook検証キーを無停止で交換する定期ローテーション作業であり、もう1つは、その日の午後に外注開発者がテスト用スクリプトを個人の公開GitHub Gistへ上げた際に、本番の領収書OCR用の外部AI APIキーが平文で露出した緊急セキュリティ事故だった。
プラットフォームセキュリティリードは、2つの事案の性質がまったく異なることを見極め、互いに異なる2つの手順を同時に指揮した。
- 露出した外部AI APIキー → 緊急失効(Emergency Revocation)手順(所要時間4分):
- すでに公開インターネットへ露出したキーは1分1秒を争うため、猶予期間を置かなかった。直ちにベンダーコンソールで新しいAPIキー(
Key_New)を発行し、VercelのProduction環境変数へ注入・再デプロイ(所要2分)して正常なOCRリクエストが新キーで出ていくようにした直後、露出した旧キー(Key_Old)をベンダーコンソールから**即座に永久削除(Revoke)**した。続いて、露出時点から失効時点までの18分間のAI Gatewayログを全件調査し、外部IPから呼び出された不正リクエストがなかったことを確認した。
- すでに公開インターネットへ露出したキーは1分1秒を争うため、猶予期間を置かなかった。直ちにベンダーコンソールで新しいAPIキー(
- セッション署名シークレットおよびWebhookキー → デュアルキー(Dual-Key)無停止ローテーション手順:
- 現在接続中の経理担当者3,000人のセッションを切ったり、リアルタイムの法人カード承認Webhookを1件たりとも取りこぼしたりしてはならないため、4段階のデュアルキー手順を踏んだ。
- まず検証コードが
[新キー, 旧キー]の配列を順番に両方とも許可するようデプロイ(第1段階)したあと、トークン署名とカード会社Webhookの送信設定を新キーへ切り替え(第2段階)た。セッション有効期限周期である24時間にわたって旧キーで検証されたリクエスト数をメトリクスで監視し、0件へ収束したことを確認(第3段階)したうえで、最後に環境変数から旧キーを完全に削除(第4段階)した。ただの1人の強制ログアウトも、ただの1件のWebhook消失もなくキーローテーションが完了した。
ヘダムクリニック:誤ったデプロイで上書きした予約データを3分前へ戻したPITRとExpand-Contract
オンライン診療の予約を支援する「ヘダムクリニック」では、火曜日の午後3時12分、予約テーブルのdoctor_nameカラムをpractitioner_idに変えてステータス値を整理するデプロイが進められた。ところが開発者が2つの致命的なミスを犯した。第1に、旧バージョンのアプリインスタンスがまだ動いている状態でALTER TABLE appointments DROP COLUMN doctor_nameをいきなり実行してしまい、旧バージョンのサーバー群が一斉に500エラーを吐き出した。第2に、データ整理マイグレーションSQLからWHERE status = 'expired'条件が抜け落ちており、午後3時12分10秒に実行されたクエリが将来の全予約14,000件のステータスをすべて'cancelled'(キャンセル済み)で上書きしてしまったのである!
アラートが鳴り響くと、インシデントコマンダー(IC)はランブックに従って即座に動いた。
| 時刻 | 段階 | 実行措置および根拠 |
|---|---|---|
| 15:13 | 隔離および出血の停止 | 追加のデータ汚染を防ぐため、予約変更のフィーチャーフラグ(Kill Switch)を即座に「メンテナンス案内(読み取り専用)」へ切り替え、アプリケーションデプロイを直前の正常バージョンへ即座にロールバック |
| 15:15 | PITRによる時点復元 | 朝の日次スナップショット(04:00)へ戻すと今日の午前〜午後の予約11時間分が蒸発してしまう。SupabaseのPITR(WALアーカイブ)を使用し、事故クエリ実行の5秒前である**15:12:05時点への精密復元**を実行 |
| 15:24 | 整合性検証および再開 | 復元されたデータにおいて14,000件の予約ステータスと午前の新規予約が1件の消失もなく完全であることを確認し、サービスを正常再開(実RTO 12分、RPO数秒を達成) |
| 翌日 | Expand-Contract再設計および振り返り | 非難のない事後振り返りを開き、(1)CIでDROP COLUMN/RENAME COLUMNがアプリコードの削除より先に実行されたらビルドを遮断するルールを追加、(2)スキーマ変更を**Expand(カラム追加) → Dual-Write & Backfill → Cutover → Contract(旧カラム削除)**の4段階に分離して無停止再デプロイを完了 |
間違えやすいポイント
- **「秘密鍵をローテーションするときは、ダッシュボードで既存の環境変数の値を新キーでそのまま上書きして再デプロイすれば無停止で終わる。」**単一のキーをそのまま上書きすると、デプロイ途中にインスタンス間の不一致が生じ、旧キーで署名されたアクティブセッションや切り替え直前の外部Webhookがすべて失敗します。必ず
[新キー, 旧キー]を同時に検証許可するデュアルキー(Dual-Key)の猶予段階を経なければなりません。 - **「毎日未明にデータベース全体のスナップショットバックアップを取っているから、午後に誤ってデータを飛ばしても何の損失もなく復旧できる。」日次スナップショットしかなければ、未明のバックアップ時刻から午後の事故時刻までに積み上がった半日分のデータは復旧できません(RPOは最大24時間)。秒・分単位での時点復元を行うには、WALベースのポイントインタイムリカバリ(PITR)**が有効になっていなければなりません。
- **「無停止ローリングデプロイがサポートされているプラットフォームなのだから、データベースのカラム名も
ALTER TABLE RENAME COLUMNで一度に変えればよい。」ローリングデプロイ中は、旧バージョンのコードと新バージョンのコードが同時に同じデータベースを参照します。カラム名をいきなり変えたり削除したりすると、旧バージョンのインスタンスが即座にエラーを出し、ロールバックも不可能になるため、必ずExpand-Contract(並行変更)**の段階を踏まなければなりません。 - **「デバッグを素早く行うには、すべてのリクエストの
headersとbodyの原文をそのまま中央ログサーバーへ残すのがよい。」**リクエストヘッダーやボディには、Authorizationトークン、セッションCookie、パスワード、APIキー、個人情報が入っているため、ログストレージがそのまま第2の流出経路になってしまいます。収集前に機密フィールドを自動でマスキング(Scrubbing)し、trace_idで追跡しなければなりません。 - **「サービスの平均応答時間(Average Latency)が200msなのだから、すべてのユーザーが快適に使えている。」高速な静的/キャッシュ応答が大多数を占めるとき、平均値は少数の極端な遅延(20秒以上かかるAI/DBボトルネック)を覆い隠してしまいます。信頼性指標は必ずp95、p99パーセンタイル(Tail Latency)**で見なければなりません。
- **「本番障害が起きたら、慌ててロールバックするのではなく、まずログを分析してなぜバグが出たのか原因を完全に突き止めてから修正パッチをデプロイすべきである。」**原因分析に1時間を費やしている間に、顧客の被害は雪だるま式に膨れ上がります。障害対応の鉄則は、先(まず)緩和・隔離(即時ロールバック、キルスイッチ、フォールバック)、後(あとで)原因分析です。
ひと目でわかるまとめ
| 重要テーマ | 試験と実務で覚えておくべき重要文 |
|---|---|
| デュアルキー (Dual-Key) ローテーション | 署名・発行は最新キー(V2)で行いつつ、検証は[V2, V1]の両方を許可する猶予期間を設けて無停止でローテーションする |
| 緊急キー失効 | キーが外部へ露出した非常事故時には、新キーのデプロイ後に猶予期間を置かず露出した旧キーを即座に永久失効(Revoke)し、監査ログを調査する |
| RPO・RTOとPITR | RPOは許容可能なデータ消失時間、RTOは目標復旧所要時間である。秒単位の復元とミスクエリの巻き戻しにはWALベースのPITRと定期復旧リハーサルが必須である |
| Expand-Contractマイグレーション | **1) 拡張(Nullable追加) → 2) 二重書き込み・バックフィル → 3) 読み取り切り替え → 4) 収縮(旧カラム削除)**の順序で後方互換性とロールバック可能性を守る |
| ゼロロックDDL | 大容量インデックスはCREATE INDEX CONCURRENTLY、制約条件はNOT VALIDのあとにVALIDATE、セッションには短いlock_timeoutを設定する |
| 可観測性とログスクラビング | メトリクス(p95/p99)、構造化ログ、分散トレース(trace_id)をつなぎ、トークン・キー・Cookie・パスワード・個人情報は収集前にマスキング(Redaction)する |
| コスト上限とサーキットブレーカー | アラート(Soft Limit)とコード遮断上限(Hard Limit)を併用し、指数バックオフ+Jitterおよびサーキットブレーカーでリトライストームとウォレット枯渇を防ぐ |
| 障害対応と事後振り返り | **緩和優先(即時ロールバック・キルスイッチ) → 原因分析・検証 → 非難のない事後振り返り(システム的な再発防止アクションアイテムの導出)**の順序を守る |
公式ドキュメントで確認する
- Supabase — Database Backups & Point-in-Time Recovery (PITR):日次物理バックアップとWALベースPITRのRPOの違いおよび復元原理。
- PostgreSQL Documentation — Routine Database Maintenance & Concurrently:
CREATE INDEX CONCURRENTLYとテーブルロックレベル。 - OpenTelemetry Documentation — Context Propagation & Collector Scrubbing:メトリクス・ログ・トレースのシグナル標準と機密データ変換プロセッサ。
- OWASP — Secrets Management Cheat Sheet:シークレット値のライフサイクルと無停止自動ローテーション設計。
- NIST — SP 800-61 Rev. 3 Incident Response Recommendations:インシデントの検知、隔離・緩和、復旧および事後教訓の反映体系。
セルフチェック
5-1. Webhook署名検証シークレットやセッション署名キーを、サービス停止やリクエスト消失なしに交換するための「デュアルキー(Dual-Key)受容パターン」の4段階を順番に説明し、反対に外部APIキーが公開GitHubへ流出したときはなぜ猶予期間を置かずに即座に失効させなければならないのか比較しなさい。
5-2. 目標復旧時点(RPO)と目標復旧時間(RTO)の意味をそれぞれ定義し、毎日午前4時にだけスナップショットを取得するデータベースにおいて午後2時に誤ってDELETEクエリを実行したとき、なぜスナップショットだけではデータ消失なしに復旧できないのかを説明したうえで、これを解決するバックアップ方式を書きなさい。
5-3. 無停止ローリングデプロイ環境で稼働中のテーブルのカラムを変更するとき、ALTER TABLE RENAME COLUMNをいきなり実行するとなぜ障害が起きてロールバックまで塞がれてしまうのかを説明し、これを解決するExpand-Contract(並行変更)の段階を順番に書きなさい。
5-4. 可観測性(Observability)において平均遅延時間(Average)の代わりにp95・p99パーセンタイル指標を見るべき理由を説明し、分散トレース(trace_id)と構造化ログを収集する際に必ず除去または伏せ字化(Scrubbing)すべき機密項目を3つ以上書きなさい。
5-5. 下位のデータベースや外部LLM APIが一時的な過負荷に陥ったとき、すべてのサーバーが短い周期で即座にリトライを繰り返すとどのような現象(Retry Storm)が起きるかを説明し、これを緩和するためのリトライ方式と遮断パターンを2つ書きなさい。
5-6. 設計課題:新規デプロイの直後に決済APIの500エラー率が15%へ跳ね上がるP1障害アラートが午前2時に鳴った。当番エンジニアが「コードをデバッグして修正パッチを新しく書きます」と言ったとき、インシデントコマンダーがなぜそれを制止すべきかを説明し、最初の10分以内に取るべき標準的な緩和措置と、障害終了後に作成する振り返り(Postmortem)の原則を記述しなさい。
実習課題
実習
Expand-Contract無停止スキーママイグレーションおよびロールバック計画書を作成する(50分)
- 現在数百万件のデータが入っている
usersテーブルにおいて、単一の文字列カラムであるphoneを、国コードが分離されたphone_countryとphone_numberの2つのカラムへ無停止で分離したい。 - 第1段階(Expand)、第2段階(Dual-Write & チャンク単位のBackfill)、第3段階(読み取り切り替えおよび
NOT VALID→VALIDATE制約条件)、第4段階(Contract)に分けたデプロイ順序表と、各段階のSQL(SET lock_timeout = '3s'を含む)を作成する。 - もし第2段階のデプロイ直後にアプリケーションのバグが発見され、旧バージョンのコードへ即座にロールバックしたとき、なぜデータ消失が発生しないのかを2文で証明する。
完了基準: 4段階のどの時点においても旧バージョンアプリと新バージョンアプリが同時に動作でき、テーブル全体を長時間ロックするDDLがなく、第2段階における無損失ロールバックの根拠が明確であること。
実習
デュアルキーローテーションコードとP1障害対応ランブックを作成する(45分)
- 環境変数
WEBHOOK_SECRETS="v2_secret,v1_secret"を読み取り、HMAC-SHA256署名を生成(常に1番目のキーを使用)し、検証(リスト内のキーの順次照合を許可し、どのキースロットが一致したかを構造化ログに記録しつつ、実際のキー値は絶対にログへ残さない)する関数を作成する。 - 次の2つのインシデントシナリオについて、5分以内の緩和措置、確認すべきダッシュボード指標(
trace_id、p99、コスト上限)、そして再発防止対策を盛り込んだ1ページのランブックを表形式で作成する:- シナリオA:外部LLM APIの連続タイムアウトによりサーバーレス関数の同時実行数が急増したとき
- シナリオB:誤ったデータベースマイグレーションのデプロイによりAPIエラー率が跳ね上がったとき
完了基準: キー検証コードに平文シークレットのロギングが一切なく旧バージョンキーの使用量を追跡でき、両方のランブックがいずれも「先緩和(フォールバック・ロールバック・サーキットブレーカー)、後原因分析」の順序に従っていること。