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。 ⚠️ ハイブリッド DB:appointments等は 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=1・wal_sender_timeout=0・shared_preload_libraries=pgaudit,pg_stat_statements・pgaudit.log=all・max_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_appointmentは PK 無しテーブル 0 / 非 default REPLICA IDENTITY 0(preflight.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-actions。BG 同期中(約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
# 変数(本番・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 json0-1. 踏み台 psql ヘルパ(本手順の psql 系で共通利用・最初に読み込む)
SSM port-forward + Secret 取得を関数化。
pgtunnel <db-host> <local-port>後にpsqlが使える。終了はpgclose <local-port>。
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 保険スナップショット(ロールバック起点)
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。
# 削除前: 単一リージョン(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=available・endpoint 不変 - [ ] 空 global cluster を削除済(
describe-global-clustersがGlobalClusterNotFoundFault)。★削除保護(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 で関係者へ周知済であること。
# 本番は 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() で確認- [ ] Writer が
rds.logical_replication=on/wal_level=logical(★BG の前提はこれ) - [ ] Writer/Reader とも
shared_preload_librariesに pgaudit を含む・pgaudit.log=all・wal_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 で作成。
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)に使う。
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秒。当日枠はこれを基準に。
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 clusterEngineVersion=16.14
6. ★Switchover 前 GO/NO-GO(preflight.md 準拠)
# ① 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=on/shared_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 実測・当日枠)
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_COMPLETED/mental-appointment=16.14 available/mental-appointment-old1=13.23 残存(保険)
8. ⑤ 事後(DB 健全性 → 2系統アプリ E2E → deep-health)
# 新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.md・staging.md §G 準拠)
mental_appointment は 2 系統が利用する(分岐は予約種別でなく
findById=v2 DB 存在有無)。両系統の接続を Switchover 前後で確認する。
consumer と接続経路
| consumer | mental_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-cluster・ap-ne-1→us-east-1 cross-region) | reader(診察履歴の固定時間予約表示) | Kysely 0.27.3 + pg(kyselyAppointment) |
| Retool / Trocco | reader(運用/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 接続エラー無し |
| ④ユーザー E2E | OPS 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で踏み台上取得(履歴に値を残さない)。bashbash 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 + 事務局アカウントのみ)。
- 事前準備:6/1 以前に「固定時間予約」を持つ患者の予約 id を1件用意(§4-1 で選んだ id でよい。mental_appointment の
appointmentsは全て 6/1 以前の固定時間予約=legacy)。 - OPS UI にログイン(事務局アカウント)→ 予約詳細を開く:
https://online-karte.fstdr.jp/ops/mental/{APPOINTMENT_ID} - 診察履歴の「予約種別」列を見る(★マーカー):
- 「固定時間予約」行(6/1以前)が表示される = online-ops が cross-region で
mental_appointment.appointmentsを read 成功(読んでいる実体)。 - 「時間帯予約」= v2(
online_ops_service)。区別する(v2 が出ても mental_appointment の確認にはならない)。
- 「固定時間予約」行(6/1以前)が表示される = online-ops が cross-region で
- Switchover 前後で同じ画面を開き、固定時間予約行が出続けることを確認(=切替で read 経路が壊れていない)。
- ⚠️ online-ops は未接続でもエラーを出さず“静かに空”になる(
read-repository.tsのif(!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 既存事象あり=補助扱い)。
- ⚠️ online-ops は未接続でもエラーを出さず“静かに空”になる(
- [ ] 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-★)。
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 SGsg-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 フルパス疎通を確認する。
- 本番の予約 id を1件取得(踏み台 psql・read-only。★Secret
mental-appointmentは制御文字混入のため JSON パースはstrict=False必須)bashpgtunnel "$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 - 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 一致で判定(本文はダンプしない)。
- ★
- ★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が出ること:bashpsql -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.199のstate_changeが probe 時刻に更新+SELECT ... patients / appointment_cancels(FindOneUsecase が読む表)を確認=mental-appointment-service ECS→Aurora read 疎通を実証。
- ① ALB ターゲット=mental-appointment-service ECS の IP 一致(別サービスでないこと):
- [ ] 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 ログで実挙動を確認するのが手軽・確実。
- Datadog Logs で検索:
service:online-ops-service "deep health overall"(本番 cluster)。 - 読み方(コード仕様
deep-health.service.ts):overall≠ok の時だけ warn ログを出し、checks.filter(c => c.status !== 'ok')=down のチェックのみ列挙する。→ ログの down リストにappointment-dbが無ければ ok。overall=ok の時は無ログ(=全チェック ok も健全)。 - 判定: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_activity の client_addr を ECS タスクへ逆引きすれば、Datadog を介さず「どの ECS が繋いでいるか」を確定できる。
# ① 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.ts の if (!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 scriptserver_version=16.x・接続元ECS属性化で mental-appointment-service / online-ops-service 両方の接続 - [ ] BG用 slot=0(
pg_replication_slots0行)/更新が必要な拡張=0 - [ ] 性能退行なし:§4 ベースラインと Top SQL 突合(怪しいものは
EXPLAIN (ANALYZE,BUFFERS)) - [ ] ★pgaudit ドリフト是正の確認:新 primary(PG16) で
SHOW shared_preload_libraries;に pgaudit・SHOW pgaudit.log;=all・CloudWatch/aws/rds/cluster/mental-appointment/postgresqlに監査ログ - [ ] 2系統アプリ E2E(OPS UI で固定時間予約 read/API プローブ)成功/Datadog
service:mental-appointment-serviceservice:online-ops-serviceエラー無し
9. ⑥ Terraform 整合(継続構成・plan no-diff)
live 使用中の
-pg16を cluster/instance PG として継続利用。詳細はpreflight.md。
# 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 planが no-diff
10. ⑦ 後片付け
# 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 作成〜ゲートまで)=無停止で安全
# 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 書込は喪失)
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 スナップショットから復元)
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 1(kyselyAppointment.ping()) がいずれもonline-ops-service-serviceのタスクから来ていた。 → §3 reboot 後・§7 Switchover 後の再接続確認は、reader だけでなく writer 側も見ること。 ※ online-ops の read 接続は idle 約10秒で回収されるため、点で見ると「不在」に見える。OPS UI を操作しながら連続実行して捕捉する。
- 検知: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-healthappointment-db(8-C) DatabaseConnectionsの回復(ダッシュボードmental-appointment-pg16-bluegreen)
- Datadog
- mental-appointment-service(Prisma・writer)復旧:エラーが数分残る場合、ローリング再起動で新規接続を強制(アプリ層は無停止)。bash
aws ecs update-service --region $R --cluster mental-appointment-cluster --service mental-appointment-service --force-new-deployment - online-ops-service(Kysely・reader・cross-region)復旧:同様に
online-ops-service-cluster/online-ops-service-serviceを ap-northeast-1 で--force-new-deployment。kyselyAppointmentの pool を張り直す。deep-healthappointment-dbの ok 復帰を確認(8-C)。 - Retool / Trocco:次のクエリ実行で自動再接続。残留なら Test connection/コンテナ再起動。
- 確認: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 を疑う。
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 差分許容の合意を先に取る。