Skip to content

mental-appointment PG16 本番 実行手順(当日オペ用・自己完結)

🏭 本番当日はこのファイルを上から順に実行する(コピペ可・実識別子入り)。 「なぜ・調査・consumer・2系統設計・findById 分岐」は preflight.md(リファレンス)を参照。Global 削除経路の素振り実績は clone-rehearsal.md。staging 実施記録は staging.md

⚠️ 停止を伴うのは §3②の瞬断(事前枠・数十秒)と §7④の書込断(当日枠・約70秒=clone 実測)のみ。他は無停止想定。 ⚠️ 破壊的操作(Global削除・reboot・Switchover・旧blue削除)は指差し確認。各節の GO 条件を満たしてから進む。 ⚠️ Writer/Reader の向きに注意:mental-appointment は -0=Writer / -1=Reader(dr-work-record とは逆)。reboot 順(Reader→Writer)は本書のとおり。 ⚠️ verify script / terraform 実行は terraform_for_aws リポジトリ(develop) のルートから。psql は §0-1 の pgtunnel を先に読み込む。AWS 権限は参照=production-admin/変更・削除=production/terraform=production-terraform。 ⚠️ ハイブリッド DBappointments 等は 2026-06-01 cutover 以降凍結(新規予約は v2=online_ops_service へ)。patients/appointment_patient_histories/doctor_appointment_blocksアクティブ。件数 diff の解釈に注意(preflight.md)。

0. 前提・変数(DoR)

  • [ ] preflight 完了(read-only・blocker なし)/ 2系統 consumer 確認(app=cluster ep✅ / online-ops は cross-region ap-ne-1→us-east-1 read / Retool/Trocco の Host 確認済 / IP直なし)
  • [ ] target/source CPG が Terraform で作成済(PR #2633 マージ済):mental-appointment-pg16(PG16・cluster/instance)/ mental-appointment-pg13-bg(PG13)。両CPGとも rds.logical_replication=1wal_sender_timeout=0shared_preload_libraries=pgaudit,pg_stat_statementspgaudit.log=allmax_replication_slots=20 を含むpreflight.md §3
  • [ ] ★pgaudit を有効化する:本番 us-east-1 の cluster は現状 default.aurora-postgresql13(custom 未適用・logical off・pgaudit 未適用)。本移行で logical=1+pgaudit.log=all を有効化(他サービスと整合・ドリフト是正)。監査ログ量が増える点は関係者へ周知しておく。
  • [ ] UPSERT/RI リスクは本 DB では該当なし(2026-08-26 本番実測で確認):mental_appointmentPK 無しテーブル 0 / 非 default REPLICA IDENTITY 0preflight.md §3-b・当日も relreplident<>'d' が 0 件で再確認)=全テーブルが PK を持ち default RI(=PK) で論理レプリケーション可能。 ⚠️ 訂正:以前ここに書かれていた「appointment_patient_histories に RI USING INDEX 適用済」は online-karte-service の mastra_workflow_snapshot(Datastream 絡みの止血・mental-online-karte#15995/#14532)の話であり、本 DB とは無関係。PG16 移行で追加対応は不要。
  • [ ] ★pending maintenance action の有無を確認aws rds describe-pending-maintenance-actionsBG 同期中(約40分)に OS パッチが適用されると予定外の再起動になる。本番実測(2026-08-26)では cluster に os-upgrade・両インスタンスに system-update が保留中。メンテナンスウィンドウ(水 00:00-01:00 UTC = 水 09:00-10:00 JST)と作業枠を重ねない
  • [ ] メンテ枠・関係者(在宅事業部=OPS UI 実機 E2E)・ロールバック担当 待機
  • [ ] 監視 mute / DNS TTL≤5s
bash
# 変数(本番・us-east-1)
export AWS_PROFILE=production-admin        # 変更系は権限に応じ production / production-terraform
R=us-east-1
CL=mental-appointment
CL_ARN=arn:aws:rds:us-east-1:967691968827:cluster:mental-appointment
WRITER=mental-appointment-0                  # ★-0=Writer(dr-work-record とは逆)。実行直前に describe で再確認
READER=mental-appointment-1
BASTION=i-09384db1d690bcfd0                  # fd-platform-bastion(us-east-1)
SECRET=mental-appointment
DBSG=sg-02088474cf99248f4                    # DB SG(参考)
SUBNET=mental-appointment-subnet            # subnet group(参考)
# Writer/Reader を実行直前に再確認(入れ替わり防止)
aws rds describe-db-clusters --region $R --db-cluster-identifier $CL \
  --query 'DBClusters[0].DBClusterMembers[].{id:DBInstanceIdentifier,writer:IsClusterWriter}' --output json

0-1. 踏み台 psql ヘルパ(本手順の psql 系で共通利用・最初に読み込む)

SSM port-forward + Secret 取得を関数化。pgtunnel <db-host> <local-port> 後に psql が使える。終了は pgclose <local-port>

bash
pgtunnel(){ HOST=$1; LPORT=$2;
  nohup aws ssm start-session --region $R --target $BASTION \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters "{\"host\":[\"$HOST\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"$LPORT\"]}" > /tmp/pgt-$LPORT.log 2>&1 &
  for i in $(seq 1 15); do grep -q "Waiting for connections" /tmp/pgt-$LPORT.log 2>/dev/null && break; sleep 1; done
  SEC=$(aws secretsmanager get-secret-value --region $R --secret-id $SECRET --query SecretString --output text)
  export PGUSER=$(echo "$SEC"  | python3 -c 'import sys,json,urllib.parse as u;d=json.load(sys.stdin);print(d.get("DB_USERNAME") or u.urlparse(d["DATABASE_URL"]).username)')
  export PGPASSWORD=$(echo "$SEC" | python3 -c 'import sys,json,urllib.parse as u;d=json.load(sys.stdin);print(d.get("DB_PASSWORD") or u.urlparse(d["DATABASE_URL"]).password)')
  export PGDATABASE=$(echo "$SEC" | python3 -c 'import sys,json,urllib.parse as u;print(u.urlparse(json.load(sys.stdin)["DATABASE_URL"]).path.lstrip("/"))')
  export PGHOST=127.0.0.1 PGPORT=$LPORT; echo "psql ready: $PGDATABASE @ $HOST (local:$LPORT)"; }
pgclose(){ pkill -f "localPortNumber.*$1" 2>/dev/null; }
# よく使う blue(現primary) / reader エンドポイント
BLUE_EP=$(aws rds describe-db-clusters --region $R --db-cluster-identifier $CL --query 'DBClusters[0].Endpoint' --output text)
READER_EP=$(aws rds describe-db-clusters --region $R --db-cluster-identifier $CL --query 'DBClusters[0].ReaderEndpoint' --output text)

⚠️ 実測のハマり(2026-08-26 本番)

  • SSM ポートフォワードが psql 1回目の直後に SIGTERM で落ちる事象が複数回発生した。1 トンネルに psql を複数投げず psql -f file.sql で 1 接続にまとめる。落ちたら local port を変えてリトライする(for attempt in 1 2 3; do ...; done)。
  • READER_EP(cluster-ro)は「利用可能な Reader が無い瞬間に Writer を指す」。Reader の reboot 直後に Reader 実体を検証する用途では インスタンス endpoint を直指定する($(aws rds describe-db-instances --db-instance-identifier $READER --query 'DBInstances[0].Endpoint.Address' --output text))。実際に cluster-ro 経由で Writer に落ち、in_recovery=false / 未再起動の pg_postmaster_start_time() を拾って誤判定しかけた。判別は pg_is_in_recovery()(Reader=true)と pg_postmaster_start_time() で行う。

1. D-0 保険スナップショット(ロールバック起点)

bash
SNAP=mental-appointment-pre-pg16-$(date +%Y%m%d%H%M)
aws rds create-db-cluster-snapshot --region $R \
  --db-cluster-identifier $CL --db-cluster-snapshot-identifier $SNAP
aws rds wait db-cluster-snapshot-available --region $R --db-cluster-snapshot-identifier $SNAP
aws rds describe-db-cluster-snapshots --region $R --db-cluster-snapshot-identifier $SNAP \
  --query 'DBClusterSnapshots[0].{id:DBClusterSnapshotIdentifier,engine:EngineVersion,status:Status}' --output json  # engine=13.23 / available
  • [ ] スナップショット available・engine=13.23

2. ① Global 削除(standalone 化・無停止想定)

mental-appointment は 単一リージョン Global(Writer 1件・secondary/reader 無し)。BG は Global では作成不可のため standalone 化する。素振り実績は clone-rehearsal.md

bash
# 削除前: 単一リージョン(writer 1件・secondary 無し)を再確認
aws rds describe-global-clusters --region $R --global-cluster-identifier $CL \
  --query 'GlobalClusters[0].GlobalClusterMembers[].{arn:DBClusterArn,writer:IsWriter,readers:Readers}' --output json
# detach → 空 global 削除
aws rds remove-from-global-cluster --region $R --global-cluster-identifier $CL --db-cluster-identifier "$CL_ARN"
aws rds wait db-cluster-available --region $R --db-cluster-identifier $CL
# ★削除保護が有効だと delete-global-cluster は失敗する(本番実測 2026-08-26):
#   InvalidParameterCombination: Cannot delete protected Global Cluster mental-appointment,
#   please disable deletion protection and try again.
aws rds describe-global-clusters --region $R --global-cluster-identifier $CL \
  --query 'GlobalClusters[0].{protection:DeletionProtection,members:length(GlobalClusterMembers)}' --output json
# ★members=0(空)を確認した上で保護解除 → 削除
aws rds modify-global-cluster --region $R --global-cluster-identifier $CL --no-deletion-protection
aws rds delete-global-cluster --region $R --global-cluster-identifier $CL
# 削除確認: GlobalClusterNotFoundFault が返るのが期待値
aws rds describe-global-clusters --region $R --global-cluster-identifier $CL
# 確認: standalone(global=null)・稼働継続・endpoint 不変
aws rds describe-db-clusters --region $R --db-cluster-identifier $CL \
  --query 'DBClusters[0].{global:GlobalClusterIdentifier,status:Status,ep:Endpoint}' --output json  # global=null / available / endpoint 据え置き
  • [ ] GlobalClusterIdentifier=null かつ Status=availableendpoint 不変
  • [ ] 空 global cluster を削除済(describe-global-clustersGlobalClusterNotFoundFault)。★削除保護(DeletionProtection=true)のため --no-deletion-protection が先に必要

ℹ️ detach さえ済めば(global=null)§3 以降・BG 作成はブロックされない(BG の制約は「DB クラスタが global の member か」で判定)。空 global が残っていても機能影響・課金はないが、clone リハと同一条件にするため BG 作成前に削除しておく(実測所要: detach 3 秒 / 保護解除+削除 数秒)。

3. ② source CPG(logical=1・pgaudit込み) 付替 → Reader→Writer reboot(★瞬断・事前枠)

★この付替+reboot で blue(PG13) に logical_replication と pgaudit を同時に有効化(現状 default PG からのドリフト是正)。監査ログ量が増える点は §0 で関係者へ周知済であること。

bash
# 本番は default CPG のため、logical=1 の custom CPG(mental-appointment-pg13-bg) へ付替
aws rds modify-db-cluster --region $R --db-cluster-identifier $CL \
  --db-cluster-parameter-group-name mental-appointment-pg13-bg --apply-immediately
aws rds wait db-cluster-available --region $R --db-cluster-identifier $CL
# pending-reboot を適用(★Reader→Writer の順=AWS推奨・安全側。計画外 failover 時に params 未適用 Reader が昇格するのを防ぐ)
aws rds reboot-db-instance --region $R --db-instance-identifier $READER   # -1=Reader を先に
aws rds wait db-instance-available --region $R --db-instance-identifier $READER
aws rds reboot-db-instance --region $R --db-instance-identifier $WRITER   # -0=Writer ★これが数十秒の瞬断
aws rds wait db-instance-available --region $R --db-instance-identifier $WRITER
# 踏み台 psql で Writer(cluster ep) と Reader(reader ep) 双方を確認:
pgtunnel "$BLUE_EP"  15432   # Writer
psql -tAc "SELECT current_setting('rds.logical_replication') AS logical, current_setting('wal_sender_timeout') AS wst, current_setting('shared_preload_libraries') AS spl, current_setting('pgaudit.log') AS pgaudit;"
pgclose 15432
# ★Reader は cluster-ro ではなく インスタンス endpoint を直指定(cluster-ro は Reader 不在の瞬間 Writer を指す・§0-1 の注意)
READER_INST_EP=$(aws rds describe-db-instances --region $R --db-instance-identifier $READER --query 'DBInstances[0].Endpoint.Address' --output text)
pgtunnel "$READER_INST_EP" 15432  # Reader 実体
psql -tAc "SELECT current_setting('rds.logical_replication') AS logical, current_setting('shared_preload_libraries') AS spl, current_setting('pgaudit.log') AS pgaudit;"
pgclose 15432
# 期待: 【Writer】logical=on / wal_level=logical / wst=0 / spl に pgaudit,pg_stat_statements / pgaudit=all
#       【Reader】logical=off / wal_level=replica ★これが正常(下の注記)/ wst=0 / spl に pgaudit / pgaudit=all
#       ※ Reader かどうかは pg_is_in_recovery()=true、再起動済みかは pg_postmaster_start_time() で確認
  • [ ] Writerrds.logical_replication=on / wal_level=logical(★BG の前提はこれ)
  • [ ] Writer/Reader とも shared_preload_librariespgaudit を含む・pgaudit.log=allwal_sender_timeout=0(ドリフト是正の反映確認)
  • [ ] ★Reader は rds.logical_replication=off / wal_level=replica が正常(writer 専用パラメータ。reader は WAL を生成しない)。Reader 側の適用確認は pgaudit のロードと wal_sender_timeout=0 で行う 根拠(2026-08-26 本番実測):同じ CPG の同じ pending-reboot パラメータである pgaudit は Reader にもロードされる一方 logical は off のまま=reboot 順序の取り残しではなく仕様。CPG は両メンバー in-sync なので、-1 が writer として起動する場合はその時点で wal_level=logical を読む
  • [ ] 実測所要(本番 2026-08-26): Reader 63 秒 → Writer 64 秒failover なし(Writer 役は -0 のまま)。付替〜reboot を通じて ECS の既存接続は自動再接続で回復し --force-new-deployment は不要だった

⚠️ pgaudit の監査ログが増え始めるのは reboot 後(2026-08-26 本番実測で誤認しかけた点) CPG を付替えた直後は pgaudit.log=all値として反映されるだけで、shared_preload_libraries に pgaudit が 未ロードのためセッション監査は動作しない。「値は入っているのに CloudWatch の監査ログが増えない」のは reboot 前なら正常。実際に増え始めるのは §3-2 の reboot 以降。 → §0 での関係者周知(監査ログ量の増加)は reboot 前で良いが、増加の確認は reboot 後に行うこと。

4. BG 作成前: blue で pg_stat_statements 拡張を作成

preload 済みだが拡張未作成。Green は同期中 read-only で後から作れないため BG 作成前に blue で作成

bash
pgtunnel "$BLUE_EP" 15432   # blue(現primary) writer endpoint
psql -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
psql -c "\dx pg_stat_statements"   # installed_version が入っていること
# ★性能退行判定用ベースライン取得(本番は実トラフィックがあるので重要。§8-6 と突合)
psql -c "\copy (SELECT queryid, query, calls, total_exec_time, mean_exec_time, rows FROM pg_stat_statements WHERE dbid=(SELECT oid FROM pg_database WHERE datname=current_database()) ORDER BY total_exec_time DESC LIMIT 50) TO '/tmp/pg_stat_baseline_ma.csv' CSV HEADER"
pgclose 15432
# ※pgaudit は CREATE EXTENSION 不要(pgaudit.log=all のセッション監査は preload+param+reboot で有効・§3済)
  • [ ] blue で pg_stat_statements 拡張 installed
  • [ ] Top50 SQL ベースライン /tmp/pg_stat_baseline_ma.csv 取得(§8-6 の退行判定用)

⚠️ 実測(2026-08-26):拡張を作成した直後は統計がほぼ空(本番でも 15 行しか取れなかった。§3 の Writer reboot でも統計はリセットされる)。§8-6 の突合に使える母数を確保するため、BG 作成(§5)直前 または Switchover 直前に取り直すこと(\copy の出力先を変えるだけ)。

4-1. (事前)read E2E 用の本番「固定時間予約」を1件選ぶ

staging 用ブックマーク 09d77b1f は staging 限定。本番は移行前に 6/1 以前の「固定時間予約」を持つ本番 appointment を1件選び、read E2E(§6/§8)に使う。

bash
pgtunnel "$BLUE_EP" 15432
# 6/1 以前・キャンセル等で編集導線のある固定時間予約を1件(id をブックマーク)
psql -c "SELECT id, patient_id, updated_at FROM appointments WHERE created_at < '2026-06-01' ORDER BY updated_at DESC LIMIT 5;"
pgclose 15432
# → 選んだ id を APPOINTMENT_ID として控える(OPS UI: https://online-karte.fstdr.jp/ops/mental/{id})
  • [ ] read E2E 用の本番固定時間予約 APPOINTMENT_ID を1件ブックマーク(OPS UI で「固定時間予約」行が表示されること)

5. ③ 標準 BG 作成(非-Global・target 16.14)→ Green 検証

★所要時間の見積り(clone 実測 2026-08-17):BG 作成 約40分・Switchover 書込停止 約70秒。当日枠はこれを基準に。

bash
CL_ARN=$(aws rds describe-db-clusters --region $R --db-cluster-identifier $CL --query 'DBClusters[0].DBClusterArn' --output text)
aws rds create-blue-green-deployment --region $R \
  --blue-green-deployment-name mental-appointment-bg \
  --source "$CL_ARN" \
  --target-engine-version 16.14 \
  --target-db-cluster-parameter-group-name mental-appointment-pg16 \
  --target-db-parameter-group-name mental-appointment-pg16
# bgid 取得(以降で使用)
BG=$(aws rds describe-blue-green-deployments --region $R \
  --query "BlueGreenDeployments[?Source=='$CL_ARN'].BlueGreenDeploymentIdentifier | [0]" --output text)
echo "BG=$BG"
# AVAILABLE 待ち(約40分)
aws rds describe-blue-green-deployments --region $R --blue-green-deployment-identifier $BG \
  --query 'BlueGreenDeployments[0].Status' --output text   # → AVAILABLE
  • [ ] BG AVAILABLE/green cluster EngineVersion=16.14

6. ★Switchover 前 GO/NO-GO(preflight.md 準拠)

bash
# ① BG AVAILABLE / StatusDetails=null / SwitchoverDetails 各 AVAILABLE
aws rds describe-blue-green-deployments --region $R --blue-green-deployment-identifier $BG \
  --query '{status:BlueGreenDeployments[0].Status,details:BlueGreenDeployments[0].StatusDetails,sw:BlueGreenDeployments[0].SwitchoverDetails[].{tgt:TargetMember,status:Status}}' --output json

# green cluster 識別子を BG(SwitchoverDetails) から取得
GREEN_CL_ARN=$(aws rds describe-blue-green-deployments --region $R --blue-green-deployment-identifier $BG \
  --query "BlueGreenDeployments[0].SwitchoverDetails[?contains(TargetMember,':cluster:')].TargetMember | [0]" --output text)
GREEN_CL=${GREEN_CL_ARN##*:cluster:}; echo "GREEN_CL=$GREEN_CL"

# ② green member CPG in-sync(engine=16.14 / cpg=in-sync 期待)
aws rds describe-db-clusters --region $R --db-cluster-identifier "$GREEN_CL" \
  --query 'DBClusters[0].{engine:EngineVersion,members:DBClusterMembers[].{id:DBInstanceIdentifier,cpg:DBClusterParameterGroupStatus}}' --output json

# ③ replica lag ≒ 0(blue=source と green の OldestReplicationSlotLag・直近の最大)
for C in $CL $GREEN_CL; do
  aws cloudwatch get-metric-statistics --region $R --namespace AWS/RDS --metric-name OldestReplicationSlotLag \
    --dimensions Name=DBClusterIdentifier,Value=$C \
    --start-time "$(date -u -v-10M +%Y-%m-%dT%H:%M:%SZ)" --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    --period 60 --statistics Maximum --query "sort_by(Datapoints,&Timestamp)[-1].Maximum" --output text | sed "s/^/$C lag=/"
done

# ④ blue 長時間tx=0(source=現 cluster endpoint へ・§0-1 の pgtunnel/BLUE_EP を使用)
pgtunnel "$BLUE_EP" 15432
psql -c "SELECT pid,state,now()-xact_start AS age,left(query,60) AS query FROM pg_stat_activity
         WHERE datname=current_database() AND state<>'idle' AND xact_start IS NOT NULL AND pid<>pg_backend_pid()
         ORDER BY xact_start;"   # ← 0 rows 期待
# ★全テーブル件数スナップショット(blue・exact count・約2.5GB の小容量なので exact で可)
ALLCOUNT_SQL="SELECT relname, (xpath('/row/c/text()', query_to_xml(format('SELECT count(*) c FROM %I.%I', schemaname, relname), false, true, '')))[1]::text::bigint FROM pg_stat_user_tables ORDER BY relname"
psql -tAF' ' -c "$ALLCOUNT_SQL" > /tmp/rowcounts_blue.txt
echo "blue 全テーブル件数 → /tmp/rowcounts_blue.txt ($(wc -l < /tmp/rowcounts_blue.txt) tables)"
pgclose 15432

# ★ Green 実接続検証(green cluster endpoint へ): server_version=16.14 / read_only=on / 件数が blue と一致
GREEN_EP=$(aws rds describe-db-clusters --region $R --db-cluster-identifier "$GREEN_CL" --query 'DBClusters[0].Endpoint' --output text)
pgtunnel "$GREEN_EP" 15433
psql -tAc "SELECT current_setting('server_version'), current_setting('default_transaction_read_only'), current_setting('shared_preload_libraries');"  # 16.14 | on | pgaudit含む
# ★全テーブル件数スナップショット(green)+ blue と diff(lag=0 のため一致が期待)
psql -tAF' ' -c "$ALLCOUNT_SQL" > /tmp/rowcounts_green.txt
echo "=== 全テーブル件数 diff(blue ↔ green・出力が空=完全一致)==="
diff /tmp/rowcounts_blue.txt /tmp/rowcounts_green.txt \
  && echo "✅ 全テーブル件数一致" \
  || echo "★差分あり: 凍結表(appointments 等)は完全一致すべき/アクティブ表(patients/appointment_patient_histories/doctor_appointment_blocks)の in-flight 数件差は許容"

# ⑤ green で ANALYZE(read_only を外したセッションで・切替後の性能事故防止)★全テーブル対象(clone リハと統一)
psql -c "SET default_transaction_read_only=off; ANALYZE VERBOSE;"
pgclose 15433
  • [ ] AVAILABLE / StatusDetails=null / SwitchoverDetails 全 AVAILABLE / CPG in-sync / lag≒0 / 長時間tx=0
  • [ ] ★Green 実接続検証(踏み台 psql・green cluster endpoint): server_version=16.14 / default_transaction_read_only=onshared_preload_libraries に pgaudit
  • [ ] ★全テーブル件数一致/tmp/rowcounts_blue.txt/tmp/rowcounts_green.txt の diff(凍結表は完全一致・アクティブ表の in-flight 数件差は許容)
  • [ ] ★ANALYZE 実施(green・全テーブル・必須)
  • [ ] 2系統 consumer(mental-appointment-service / online-ops-service / Retool / Trocco)が cluster/reader ep 接続(Switchover でインスタンス名変化に非依存)

7. ④ Switchover(★書込断 約70秒=clone 実測・当日枠)

bash
aws rds switchover-blue-green-deployment --region $R \
  --blue-green-deployment-identifier $BG --switchover-timeout 300
# 完了ポーリング
while :; do S=$(aws rds describe-blue-green-deployments --region $R --blue-green-deployment-identifier $BG --query 'BlueGreenDeployments[0].Status' --output text); echo $S; \
  [ "$S" = SWITCHOVER_COMPLETED ] && break; [ "$S" = SWITCHOVER_FAILED ] && { echo FAILED; break; }; sleep 15; done
# 確認: 実体が 16.14・旧は -old1 にリネーム
aws rds describe-db-clusters --region $R \
  --query "DBClusters[?contains(DBClusterIdentifier,'mental-appointment')].{id:DBClusterIdentifier,engine:EngineVersion,status:Status}" --output json

⚠️ StatusDetails の型が完了時に変わる(staging 実測):SWITCHOVER_IN_PROGRESS 中は配列/null だが、完了時に文字列 "Switchover completed" になる。JMESPath で join(\,`, StatusDetails)を使うポーリングは完了時にIn function join(), invalid type for valueで壊れる。**ポーリングは上記のとおりStatus だけを見る**(StatusDetails` を素で出すのは可)。

  • [ ] SWITCHOVER_COMPLETEDmental-appointment=16.14 available/mental-appointment-old1=13.23 残存(保険)

8. ⑤ 事後(DB 健全性 → 2系統アプリ E2E → deep-health)

bash
# 新primary の cluster endpoint を再取得(Switchover 後・identifier は不変)
NEW_EP=$(aws rds describe-db-clusters --region $R --db-cluster-identifier $CL --query 'DBClusters[0].Endpoint' --output text)
pgtunnel "$NEW_EP" 15432
# ★昇格確認(read-only ガードを付けずに実行=ro の本来値を見る。staging §6① 準拠)
psql -c "SELECT current_setting('server_version') AS ver, pg_is_in_recovery() AS in_recovery,
  current_setting('default_transaction_read_only') AS ro, current_setting('rds.logical_replication') AS logical,
  current_setting('wal_level') AS wal_level;"
#   期待: ver=16.14 / in_recovery=f / ★ro=off(昇格成功の決定的証拠。同期中の green は on)/ logical=on / wal_level=logical
# Switchover 前に green で取った ANALYZE 統計が引き継がれているか(16/16 期待)
psql -c "SELECT count(*) AS tables, count(last_analyze) AS analyzed, max(last_analyze) FROM pg_stat_user_tables;"
# pg_stat_statements を PG16 向けに UPDATE
psql -c "ALTER EXTENSION pg_stat_statements UPDATE;"
psql -tAc "SELECT extname||' '||extversion FROM pg_extension WHERE extname='pg_stat_statements';"   # → 1.10 系
# pgaudit ドリフト是正の確認(新primary)
psql -c "SELECT name,setting FROM pg_settings WHERE name IN ('pgaudit.log','rds.logical_replication','shared_preload_libraries');"  # pgaudit.log=all / logical=on / spl に pgaudit
# 8-4 更新が必要な拡張が残っていないか(0行が正)
psql -c "SELECT name, default_version, installed_version FROM pg_available_extensions WHERE installed_version IS NOT NULL AND installed_version <> default_version;"
# 8-5 BG 用レプリケーション slot が消えていること(0行が正)
psql -c "SELECT slot_name, slot_type, active FROM pg_replication_slots;"
# 8-6 性能退行チェック: Top SQL を取得し §4 ベースラインと突合
psql -c "\copy (SELECT queryid, query, calls, total_exec_time, mean_exec_time, rows FROM pg_stat_statements WHERE dbid=(SELECT oid FROM pg_database WHERE datname=current_database()) ORDER BY total_exec_time DESC LIMIT 50) TO '/tmp/pg_stat_after_ma.csv' CSV HEADER"
# diff /tmp/pg_stat_baseline_ma.csv /tmp/pg_stat_after_ma.csv で mean_ms 悪化を確認。怪しいものは EXPLAIN (ANALYZE,BUFFERS)
pgclose 15432
# 書込/読取 probe(verify script・read + 接続元ECS属性化 + writability)
bash docs/sre/scripts/pg16-verify-mental-appointment.sh --env production --label post
# pgaudit 監査ログが CloudWatch に出ているか(新primary・直近5分の AUDIT 行数)
aws logs filter-log-events --region $R --log-group-name /aws/rds/cluster/$CL/postgresql \
  --start-time $(( ($(date +%s) - 300) * 1000 )) --filter-pattern 'AUDIT' \
  --query 'length(events)' --output text   # >0 なら監査ログ出力あり

8-A. 2系統アプリ E2E(★本移行の主眼・preflight.mdstaging.md §G 準拠)

mental_appointment は 2 系統が利用する(分岐は予約種別でなく findById=v2 DB 存在有無)。両系統の接続を Switchover 前後で確認する。

consumer と接続経路

consumermental_appointment への接続driver
mental-appointment-service(ECS mental-appointment-cluster・us-east-1)writer(患者情報編集の legacy 書込)Prisma 5.13.0
online-ops-service(ECS online-ops-service-clusterap-ne-1→us-east-1 cross-regionreader(診察履歴の固定時間予約表示)Kysely 0.27.3 + pgkyselyAppointment
Retool / Troccoreader(運用/ETL)JDBC

4層で確認(本番・production-admin

確認方法
①インフラECS mental-appointment-service / online-ops-service-service RunningCount=Desired / ALB target healthy。⚠️ online-ops は未接続でも healthy になる(下記 8-D の graceful degradation の罠)ので復帰判定に使わない
②API/ヘルスmental-appointment-service は API 直プローブ(DB read エンドポイント・8-A の②)。⚠️ /health は liveness のみ。online-ops は deep-health(appointment-db・8-C)+ OPS UI の固定時間予約行表示(8-D 参照)
③APM/ログDatadog service:mental-appointment-service / service:online-ops-service の Errors で Prisma/pg 接続エラー無し
④ユーザー E2EOPS UI 実機online-karte.fstdr.jp/ops/mental/{APPOINTMENT_ID})で固定時間予約 read。方針=接続確認(両 ECS が Aurora に繋がることを read で確認・write は任意)
  • [ ] online-ops-service 接続確認(read・cross-region):OPS UI https://online-karte.fstdr.jp/ops/mental/{APPOINTMENT_ID}(§4-1 で選んだ本番固定時間予約)を開き、診察履歴に「固定時間予約(6/1以前)」行が表示される(=online-ops が cross-region で mental_appointment.appointments を read 成功)。壊れると 6/1以前履歴が欠落。補助として pg_stat_activity で online-ops タスク IP の SELECT appointments を連続 poll で捕捉(idle 約10秒で回収されるため 1〜2秒間隔)。
  • [ ] mental-appointment-service 接続確認(read・主手段=API 直プローブ)GET /v1/admin/appointments/{APPOINTMENT_ID} が 200+予約 JSON。※/health は liveness のみで DB を叩かず不可。前提:踏み台→ALB(80) SG(#17398)=手動 apply 必須(下記 8-B)。CAT は --cat-secret で踏み台上取得(履歴に値を残さない)。
    bash
    bash docs/sre/scripts/pg16-verify-mental-appointment.sh --env production --label post --api-probe --cat-secret <admin CAT secret>
  • [ ] (任意)write E2E:接続確認が目的なら不要。切替後に書けるかまで見る場合のみ、編集導線のある legacy 固定時間予約の患者情報を OPS UI で編集→appointment_patient_histories に新規行追記+pg_stat で mental-appointment-service タスク IP の COMMIT 一致(preflight.md の staging 実証と同手順)。
  • [ ] 接続断→再接続:Switchover 時に mental-appointment-service / online-ops-service がクラッシュ/スタックしない(cluster endpoint 接続で自動再接続想定・残留時 §11)。**特に online-ops は cross-region(ap-ne-1→us-east-1)**のため SG/peering 影響に注意(deep-health で検知・下記 8-C)。

8-A-1. ★online-ops → mental_appointment を UI で確認する手順(具体・staging.md §G 準拠)

online-ops が mental_appointment を read しているのは OPS の診察履歴に出る「固定時間予約(6/1以前)」行。これが出続ければ read 経路(kyselyAppointment.replica(true).selectFrom('appointments'))が生存。SG も CAT も不要(UI + 事務局アカウントのみ)。

  1. 事前準備:6/1 以前に「固定時間予約」を持つ患者の予約 id を1件用意(§4-1 で選んだ id でよい。mental_appointment の appointments は全て 6/1 以前の固定時間予約=legacy)。
  2. OPS UI にログイン(事務局アカウント)→ 予約詳細を開く: https://online-karte.fstdr.jp/ops/mental/{APPOINTMENT_ID}
  3. 診察履歴の「予約種別」列を見る(★マーカー)
    • 「固定時間予約」行(6/1以前)が表示される = online-ops が cross-region で mental_appointment.appointments を read 成功(読んでいる実体)。
    • 「時間帯予約」= v2(online_ops_service)。区別する(v2 が出ても mental_appointment の確認にはならない)。
  4. Switchover 前後で同じ画面を開き、固定時間予約行が出続けることを確認(=切替で read 経路が壊れていない)。
    • ⚠️ online-ops は未接続でもエラーを出さず“静かに空”になるread-repository.tsif(!isConnected()) return []・§8-D)。行が消えたら未接続を疑う(Datadog Errors だけ見ていると気づけない)。
    • 補助:pg_stat_activity で online-ops タスク IP の SELECT ... appointments(★idle 約10秒で回収されるため UI 操作中に 1〜2秒間隔で連続 poll)/deep-health(§8-C・timeout 既存事象あり=補助扱い)。
  • [ ] Switchover 後、OPS UI の診察履歴に「固定時間予約(6/1以前)」行が表示される(=online-ops→mental_appointment read 生存)

8-B. API 直プローブ用 SG(#17398・★手動 apply 必須)

sg-associations/CI の apply 対象外の独立 terraform root(別 state)。マージだけでは適用されないので、本番も踏み台→online-ops/mental-appointment ALB(:80) の SG を手動 applyする(staging 実績=staging.md §G-★)。

bash
cd fastdoctor-template/pf-mental-appointment-service/production/sg-associations   # ※本番 SG root(#17398)
export AWS_PROFILE=production-terraform
terraform init -reconfigure
# ⚠️ 実測(2026-08-24)のハマり:
#  (a) このrootには廃止済 pf-mental-interview-service(#1631/#1638)への dangling 参照が残り full plan が落ちる
#      → 今回のルールだけを -target で apply する:
terraform plan -target='module.bastion-to-alb-sg' -out plan.tfplan   # 「1 to add(bastion→ALB:80)」を確認
terraform apply plan.tfplan
#  (b) ローカルが古いと bastion-to-alb.tf が無く plan が「No changes」になる → 先に develop を pull すること。
  • [ ] apply 後、sgr-...(ALB SG sg-0d5e9ddfe26fd81be に 踏み台 sg-09a3dec4eb92eebf0 からの 80 ingress・description に #17398)を確認。

8-B-1. ★API 直プローブ 実行手順(SG apply 後・本番実測 2026-08-24)

GET /v1/admin/appointments/{id} → FindOneUsecase → Prisma → mental_appointment Aurora の read フルパス疎通を確認する。

  1. 本番の予約 id を1件取得(踏み台 psql・read-only。★Secret mental-appointment は制御文字混入のため JSON パースは strict=False 必須)
    bash
    pgtunnel "$BLUE_EP" 15432   # §0-1
    psql -tAc "SET default_transaction_read_only=on; SELECT id FROM appointments ORDER BY created_at DESC LIMIT 1;"
    pgclose 15432
  2. api-probe(GET のみ・read・副作用なし)
    bash
    bash docs/sre/scripts/pg16-verify-mental-appointment.sh --env production --api-probe \
      --appointment-id <取得した id> --cat pg16-probe
    • /v1/admin/appointments/:id@UseGuards 無し(internal ALB+SG が境界)=x-access-token は検証されない → ダミーで可(有効な admin CAT は不要)。
    • 期待:200 + 予約 JSON=mental-appointment-service ECS → Aurora(writer ep) read 疎通。応答は患者 PII を含むため http_code と 予約 id 一致で判定(本文はダンプしない)。
  3. empirical 裏取り(「その ECS が」「その Aurora を」読んだ特定)
    • ① ALB ターゲット=mental-appointment-service ECS の IP 一致(別サービスでないこと):describe-target-health の登録 IP = aws ecs ... mental-appointment-service のタスク IP と一致することを確認。
    • ② api-probe 直後の pg_stat:当該 ECS IP の state_change が probe 時刻に更新され、appointment 集約表(patients / appointment_cancels 等)の SELECT が出ること:
      bash
      psql -c "SET default_transaction_read_only=on;
        SELECT host(client_addr), state, state_change, left(regexp_replace(query,'\s+',' ','g'),80)
        FROM pg_stat_activity WHERE host(client_addr) IN (<mental-appointment-service ECS の IP...>)
        ORDER BY state_change DESC LIMIT 8;"
    • 実測(2026-08-24):ALB TG=10.0.3.199/10.0.3.201/10.0.2.112(=ECS タスク IP 一致)。api-probe 3連射(200×3)直後、10.0.2.112/10.0.3.199state_change が probe 時刻に更新+SELECT ... patients / appointment_cancels(FindOneUsecase が読む表)を確認=mental-appointment-service ECS→Aurora read 疎通を実証
  • [ ] api-probe が 200+予約 JSON(id 一致)/ ② で当該 ECS IP の直近 SELECT(appointment 表)を確認

8-C. ★deep-health appointment-db:ok を Datadog ログで確認(#17624・SG/ECS Exec 不要・推奨)

online-ops-service の deep-health に appointment-db インジケータkyselyAppointment.ping()=mental_appointment へ SELECT 1)が入っている(#17624)。HTTP 直叩き(GET /health/dependencies)は SG/ECS Exec が要るため、Datadog ログで実挙動を確認するのが手軽・確実。

  1. Datadog Logs で検索:service:online-ops-service "deep health overall"(本番 cluster)。
  2. 読み方(コード仕様 deep-health.service.ts):overall≠ok の時だけ warn ログを出し、checks.filter(c => c.status !== 'ok')down のチェックのみ列挙する。→ ログの down リストに appointment-db が無ければ ok。overall=ok の時は無ログ(=全チェック ok も健全)。
  3. 判定:Switchover 後の時間帯で appointment-db が down リストに出ないこと(cross-region read が切れれば appointment-db が down として列挙される)。
  • [ ] deep-health ログで Switchover 後に appointment-db が down 列挙されない(=ok)。※ version タグは release PR 次第で据え置きのことがあるが image sha で稼働コードを確認可。

★代替(より直接的・本番実測 2026-08-26):DB 側から pg_stat_activityclient_addrECS タスクへ逆引きすれば、Datadog を介さず「どの ECS が繋いでいるか」を確定できる。

bash
# ① reader/writer 実体で 10.10.x の接続を確認(reader=Kysely の select "appointments"… / writer=SELECT 1 の ping)
# ② その IP の ENI の SG を確認 → online-ops-service-sg なら online-ops の ECS タスク
aws ec2 describe-network-interfaces --region ap-northeast-1 \
  --filters Name=addresses.private-ip-address,Values=<IP> \
  --query 'NetworkInterfaces[].[join(`,`,Groups[].GroupName),Description]' --output text
# ③ 稼働タスクの privateIPv4 一覧と突合(describe-tasks は一度に多数渡すと失敗するので分割)
aws ecs list-tasks --region ap-northeast-1 --cluster online-ops-service-cluster \
  --service-name online-ops-service-service --query 'taskArns' --output text | tr '\t' '\n' > /tmp/oo_tasks.txt
split -l 10 /tmp/oo_tasks.txt /tmp/oo_chunk_
for f in /tmp/oo_chunk_*; do
  aws ecs describe-tasks --region ap-northeast-1 --cluster online-ops-service-cluster \
    --tasks $(tr '\n' ' ' < $f) \
    --query 'tasks[].attachments[].details[?name==`privateIPv4Address`].value' --output text | tr '\t' '\n'
done | sort -u

実測: reader 側の Kysely クエリと writer 側の SELECT 1 はいずれも online-ops-service-service(SG online-ops-service-sg)のタスクからだった。api-probe(§8-B-1)は mental-appointment-service の ALB を叩くため online-ops はカバーしない点に注意。

8-D. ★online-ops の graceful degradation の罠(判定を誤りやすい・staging.md §G-3 準拠)

⚠️ online-ops は「未接続でもエラーを出さず静かに空になる」。ECS/ALB のヘルスや Datadog Errors だけ見ていると「繋がっている」と誤判定する。**復帰判定の主手段は「OPS UI に固定時間予約(6/1以前)の行が表示されること」**にする。

観測意味
OPS UI の診察履歴から 固定時間予約(6/1以前)の行が消えるonline-ops が mental_appointment に繋がっていない。read-repository.tsif (!isConnected()) return [] ガードで例外を出さず静かに空になる(preflight.md)=エラーログが出ず見落としやすい
ECS RunningCount=Desired・ALB target healthy⚠️ online-ops は未接続でもこうなるisConnected() は env の有無だけで立てるフラグ・起動時ヘルスチェックはコメントアウト)。復帰判定に使わない
Datadog Appointment database pools initialized successfully⚠️ pool を作れただけ=到達性の証跡にならない
deep-health の down リストに appointment-db実接続 NG(online-ops で唯一の実接続確認・8-C)

⚠️ deep-health appointment-db: down (timeout) の既存事象(staging 2026-08-18 17:22 以降):DB 側は健全(reader ep へ 0.12 秒で応答・SELECT 1 は完了)なのにアプリが 2 秒で timeout 判定する事象があった(pool の idle 回収による毎回コールドスタートが疑い)。deep-health が down でも OPS UI に固定時間予約行が出ていれば read 経路は生存と判断してよい(staging Switchover 前後で実証)。本番でも deep-health は補助・UI 表示を主手段にする。

  • [ ] ALTER EXTENSION … UPDATE 完了/verify script server_version=16.x・接続元ECS属性化で mental-appointment-service / online-ops-service 両方の接続
  • [ ] BG用 slot=0pg_replication_slots 0行)/更新が必要な拡張=0
  • [ ] 性能退行なし:§4 ベースラインと Top SQL 突合(怪しいものは EXPLAIN (ANALYZE,BUFFERS)
  • [ ] ★pgaudit ドリフト是正の確認:新 primary(PG16) で SHOW shared_preload_libraries;pgauditSHOW pgaudit.log;=all・CloudWatch /aws/rds/cluster/mental-appointment/postgresql に監査ログ
  • [ ] 2系統アプリ E2E(OPS UI で固定時間予約 read/API プローブ)成功/Datadog service:mental-appointment-service service:online-ops-service エラー無し

9. ⑥ Terraform 整合(継続構成・plan no-diff)

live 使用中の -pg16 を cluster/instance PG として継続利用。詳細は preflight.md

bash
# 1) コード変更(別PR)を develop にマージ:
#    production/main.tf の DB モジュール呼び出しを
#      rds_family="aurora-postgresql16"
#      cluster_parameter_group_custom_enable=false
#      cluster_parameter_group_name  = module.pg16_blue_green_param_groups.cluster_parameter_group_name
#      instance_parameter_group_name = module.pg16_blue_green_param_groups.db_parameter_group_name
#      create_instance_parameter_group=false
#    + tfvars/変数 rds_engine_version=16.14、pg16_param_groups.tf から pg13_source(mental-appointment-pg13-bg) モジュール撤去
# 2) 本番 tfstate に対して import → plan(apply は CI/手動で)
cd fastdoctor-template/pf-mental-appointment-service/production
export AWS_PROFILE=production-terraform
./download-tfvar.sh
terraform init -input=false -reconfigure
# plan で -pg16 が add(state 未登録・AWS 実在)になる場合は先に import:
terraform import -var-file=terraform.tfvars \
  'module.pg16_blue_green_param_groups.aws_rds_cluster_parameter_group.target' mental-appointment-pg16
terraform import -var-file=terraform.tfvars \
  'module.pg16_blue_green_param_groups.aws_db_parameter_group.target'          mental-appointment-pg16
# no-diff 確認
terraform plan -var-file=terraform.tfvars
  • [ ] tfvars/変数の rds_engine_version=16.14、rds_family="aurora-postgresql16"cluster_parameter_group_custom_enable=false、cluster/instance PG を module.pg16_blue_green_param_groups 出力へ、create_instance_parameter_group=false
  • [ ] -pg16(cluster CPG/instance PG) を terraform import(plan で add になる場合)
  • [ ] max_replication_slots は CPG で 20 明示(source 実効値と一致・preflight.md §3
  • [ ] terraform planno-diff

10. ⑦ 後片付け

bash
# BG デプロイリソース削除(--delete-target は付けない=クラスタは残す)
aws rds delete-blue-green-deployment --region $R --blue-green-deployment-identifier $BG
# 安定確認後(保険期間経過後)に旧 blue を削除
aws rds modify-db-cluster --region $R --db-cluster-identifier mental-appointment-old1 --no-deletion-protection --apply-immediately
# ★本番は Writer+Reader の 2 台 → old1 の全メンバーを削除してから cluster 削除(1台だけ消すと delete-db-cluster が失敗)
OLD_MEMBERS=$(aws rds describe-db-clusters --region $R --db-cluster-identifier mental-appointment-old1 \
  --query 'DBClusters[0].DBClusterMembers[].DBInstanceIdentifier' --output text)
echo "old1 members = $OLD_MEMBERS"   # 例: mental-appointment-0-old1 mental-appointment-1-old1
for i in $OLD_MEMBERS; do aws rds delete-db-instance --region $R --db-instance-identifier "$i" --skip-final-snapshot; done
for i in $OLD_MEMBERS; do aws rds wait db-instance-deleted --region $R --db-instance-identifier "$i"; done
aws rds delete-db-cluster --region $R --db-cluster-identifier mental-appointment-old1 \
  --final-db-snapshot-identifier mental-appointment-old1-final-$(date +%Y%m%d)
# source用 CPG(mental-appointment-pg13-bg) は旧blue削除後に孤児化 → 削除(Terraform から撤去も同時に)
aws rds delete-db-cluster-parameter-group --region $R --db-cluster-parameter-group-name mental-appointment-pg13-bg
  • [ ] BG リソース削除/旧blue削除(保険期間後)/source CPG 削除/監視ダッシュボード撤去(別PR・#17414 の module ブロック削除→apply)
  • [ ] (任意)de-Global 後のダウンサイズ検討(単一リージョンなら Global 復元は通常不要)

11. ロールバック

⚠️ Switchover 後の切り戻しは、切替後に新primary(16.14)へ入った書込が失われる(old1/スナップショットは切替時点で更新停止)。実行前に 判断者・連絡経路・監視復帰(mute解除)書込停止 or 差分許容の合意を取る。BG は一方向=逆 Switchover 不可。2系統とも consumer を手動で戻す(mental-appointment-service の secret/online-ops-service の APPOINTMENT_DATABASE_URL)。

A) Switchover 前(BG 作成〜ゲートまで)=無停止で安全

bash
# BG を破棄すれば blue(13.23) が無傷で継続(--delete-target は付けない)
aws rds delete-blue-green-deployment --region $R --blue-green-deployment-identifier $BG
# (任意)§3 の CPG 付替/pgaudit を戻すなら default CPG へ戻して reboot(★瞬断・Reader→Writer)
# (任意)§2 で外した Global を戻すなら create-global-cluster(単一リージョンなら通常不要)

B) Switchover 後・旧blue(mental-appointment-old1) 未削除(★post-switchover 書込は喪失)

bash
aws rds describe-db-clusters --region $R --db-cluster-identifier mental-appointment-old1 \
  --query 'DBClusters[0].{engine:EngineVersion,status:Status,writer:Endpoint,reader:ReaderEndpoint}' --output json
OLD1_EP=$(aws rds describe-db-clusters --region $R --db-cluster-identifier mental-appointment-old1 --query 'DBClusters[0].Endpoint' --output text)
echo "OLD1 writer=$OLD1_EP"
# consumer を old1 エンドポイントへ向け直す(rename しても endpoint hash が変わり元 DNS は戻らない=secret 更新が必要)
# 1) mental-appointment-service: DATABASE_URL(secret=mental-appointment) の host を $OLD1_EP に更新 → ECS 強制再デプロイ
aws ecs update-service --region $R --cluster mental-appointment-cluster --service mental-appointment-service --force-new-deployment
# 2) online-ops-service: APPOINTMENT_DATABASE_URL / APPOINTMENT_REPLICA_DATABASE_URL の host を $OLD1_EP(reader) に更新 → ECS 強制再デプロイ(cross-region)
aws ecs update-service --region ap-northeast-1 --cluster online-ops-service-cluster --service online-ops-service-service --force-new-deployment
# 3) 13.23(old1) に戻ったこと・両系統疎通を確認(verify script / deep-health / Datadog)

C) Switchover 後・old1 も削除済(D-0 スナップショットから復元)

bash
SNAP=$(aws rds describe-db-cluster-snapshots --region $R --snapshot-type manual \
  --query "reverse(sort_by(DBClusterSnapshots[?contains(DBClusterSnapshotIdentifier,'mental-appointment-pre-pg16')],&SnapshotCreateTime))[0].DBClusterSnapshotIdentifier" --output text)
echo "restore from $SNAP"
aws rds restore-db-cluster-from-snapshot --region $R \
  --db-cluster-identifier mental-appointment-restore \
  --snapshot-identifier "$SNAP" --engine aurora-postgresql --engine-version 13.23
aws rds create-db-instance --region $R --db-instance-identifier mental-appointment-restore-0 \
  --db-cluster-identifier mental-appointment-restore --engine aurora-postgresql --db-instance-class db.r7g.large
aws rds wait db-instance-available --region $R --db-instance-identifier mental-appointment-restore-0
# → 2系統 consumer を restore クラスタ endpoint へ向け直す(B と同手順で secret 更新+再デプロイ)

12. 接続残留時の復旧手順(§3 reboot / §7 Switchover 共通)

アプリ・Retool・Trocco とも cluster endpoint 接続(DNS は再起動/切替で不変)→ 基本は自動再接続で自己回復。復旧作業が要るのは、プールが切れた接続を掴んだままエラーを出し続ける残留ケースのみ。(staging 版は staging.md §G-2

⚠️ online-ops は writer / reader の両方に接続を持つ(2026-08-26 本番実測) preflight.md §3 に「Writer に 10.10.x(online-ops)不在」という記述があるが、これは 点観測での取り逃し。 実測では reader 側に Kysely の select "appointments"…、writer 側に deep-health の SELECT 1kyselyAppointment.ping() がいずれも online-ops-service-service のタスクから来ていた。 → §3 reboot 後・§7 Switchover 後の再接続確認は、reader だけでなく writer 側も見ること。 ※ online-ops の read 接続は idle 約10秒で回収されるため、点で見ると「不在」に見える。OPS UI を操作しながら連続実行して捕捉する。

  1. 検知:reboot/Switchover 直後の一時的な接続エラーは数分で消えれば正常。残るかを見る。
    • Datadog service:mental-appointment-service / service:online-ops-service(本番)の Errors(Can't reach database server / Connection terminated / server closed the connection / terminating connection due to administrator command)/deep-health appointment-db(8-C)
    • DatabaseConnections の回復(ダッシュボード mental-appointment-pg16-bluegreen
  2. mental-appointment-service(Prisma・writer)復旧:エラーが数分残る場合、ローリング再起動で新規接続を強制(アプリ層は無停止)。
    bash
    aws ecs update-service --region $R --cluster mental-appointment-cluster --service mental-appointment-service --force-new-deployment
  3. online-ops-service(Kysely・reader・cross-region)復旧:同様に online-ops-service-cluster / online-ops-service-serviceap-northeast-1--force-new-deploymentkyselyAppointment の pool を張り直す。deep-health appointment-db の ok 復帰を確認(8-C)。
  4. Retool / Trocco:次のクエリ実行で自動再接続。残留なら Test connection/コンテナ再起動。
  5. 確認:verify script/踏み台 psql で 2系統疎通、OPS UI に固定時間予約行表示(online-ops)、Datadog Errors 解消、DatabaseConnections 平常化。

12-B. ECS が Aurora に接続できない場合のトリアージ(--force-new-deployment で直らない時・staging.md §G-3 準拠)

**「残留(古い接続を掴んでいる)」ではなく「到達できない」**を切り分ける。①②で DB 単体の生死を確定してから SG/endpoint を疑う。

bash
export AWS_PROFILE=production-admin; R=us-east-1; CL=mental-appointment

# ① クラスタ状態と endpoint(Switchover 後は writer/reader が green 実体を指す・DNS 名は不変)
aws rds describe-db-clusters --region $R --db-cluster-identifier $CL \
  --query 'DBClusters[0].{engine:EngineVersion,status:Status,writer:Endpoint,reader:ReaderEndpoint,cpg:DBClusterParameterGroup}'

# ② 踏み台 psql で DB へ直接(アプリを介さず DB 単体の生死・書込可否・接続元ECS を一括確認)
bash docs/sre/scripts/pg16-verify-mental-appointment.sh --env production --label post
#   ⚠️ Secret `mental-appointment` は値に制御文字を含む=JSON パースは strict=False 必須

# ③ DB SG inbound(5432) に両 ECS の SG が残っているか(本番 DB SG=sg-02088474cf99248f4)
aws ec2 describe-security-groups --region $R --group-ids sg-02088474cf99248f4 \
  --query 'SecurityGroups[0].IpPermissions[?FromPort==`5432`].UserIdGroupPairs[].GroupId' --output text
#   期待: mental-appointment-service / online-ops-service の SG を含む(実 id は当日 describe で確認)

# ④ ECS が見ている接続先(secret の host が blue/green どちらを指すか)
#    mental-appointment-service … secret `mental-appointment` の DATABASE_URL(=cluster/writer ep)
#    online-ops-service        … secret の APPOINTMENT_DATABASE_URL(writer) / APPOINTMENT_REPLICA_DATABASE_URL(cluster-ro)
#    ★online-ops の実接続は reader のみ(cross-region ap-ne-1→us-east-1)→ reader ep の到達性を先に見る
  • ②で落ちる → DB 側(BG 同期中の read-only/green 未昇格/status)。②の writability(pg_is_in_recovery()=false / default_transaction_read_only=off)を確認。※Switchover 前の同期中は green が read-only が正常。
  • ②が通るのに ECS だけ繋がらない → SG / endpoint / secret 側。③④の順に潰す。特に online-ops は cross-regionなので、Switchover で peering/SG に影響が出ていないか(reader ep への到達)を重点確認。
  • 打ち切り判断:Switchover 前なら BG 破棄で blue(13.23) 継続=安全撤退(§11 A)。Switchover 後は §11 B/C(★post-switchover 書込喪失・BG は逆不可)。判断者・書込停止 or 差分許容の合意を先に取る。