PG16 アップグレード preflight:mental-appointment(★Global Database・本番)
本書は mental-appointment 本番の PG16 移行前 preflight(read-only)調査結果とその手法をまとめたリファレンス。 実施手順本体(clone リハ/staging/本番当日)は別ファイル(作成予定 #17390 / #17391 / #17392)。本書は なぜ/現状値/consumer 棚卸し/アプリ互換/E2E 疎通の方法 を記録する。
📚 mental-appointment 手順書セット(予定):
- 📖 本書(
services/mental-appointment/preflight.md)= preflight リファレンス:調査結果・現状値・consumer・アプリ互換・E2E 方法- 🧪 clone リハ手順
mental-appointment/clone-rehearsal.md(#17390)/ 🧪 staging 手順mental-appointment/staging.md(#17391)/ 🏭 本番当日手順mental-appointment/production.md(#17392)方針:dr-work-record と同じく Global Database を削除して standalone 化 → 標準(非-Global)Blue/Green(詳細は dr-work-record.md §6)。ただし mental-appointment は us-east-1 / ap-northeast-1 両リージョンにアプリ(ECS/ALB)が存在する点が異なる(§3・§5 参照)。
対象サービス: mental-appointment / 親イシュー: #17280 / preflight イシュー: #17281 / 最終更新: 2026-08-13 関連: staging CPG #17388 / 本番 CPG #17389 / clone 手順 #17390 / staging 手順 #17391 / 本番手順 #17392 / 踏み台→ALB SG(staging)terraform_for_aws#2621
★0. 最重要: mental_appointment DB はハイブリッド(凍結+アクティブ)— 2026-06-01 に予約本体が v2 へ移行済(2026-08-13 実測)
移行 E2E 設計を左右する核心。必読。
2026-06-01 の「時間帯予約(v2) 全面展開」で、メンタル予約の本体は別系統(v2)へ移行した。移行対象の mental_appointment DB(本番・staging とも)は以下のハイブリッド状態:
| 面 | テーブル | 本番の最新 created_at | 状態 |
|---|---|---|---|
| 🟢 アクティブ(継続書込) | patients | 2026-08-13(当日) | 継続 |
| 🟢 アクティブ | doctor_appointment_blocks(医師枠) | 2026-08-13(当日) | 継続 |
| 🟢 アクティブ | appointment_patient_histories | 2026-08-09 | 継続 |
| 🔴 凍結(legacy) | appointments / appointment_histories / appointment_required_actions / appointment_health_insurance_histories | 2026-06-01(全面展開日で停止) | 読取のみ |
| 🔴 凍結 | appointment_online_videos / clius_kartes | 2026-05-31 | 読取のみ |
| 🔴 凍結 | appointment_cancels(6/02) / pick_up_pharmacy_histories(6/09) | 〜6月上旬 | 読取のみ |
新規の時間帯予約(v2)は mental_appointment DB に書かれない(別系統)。v2 予約の実ストア=online_ops_service DB(online-ops-service 自前DB・Aurora PG 16.11・既にPG16) と特定。テーブル: "Appointment" / TimeSlotReservation / TimeSlotAppointmentState / DoctorTimeSlotAppointmentShift / AppointmentPatient / AppointmentDoctorAssignment / AppointmentVideo / AppointmentNotificationSent 等(Prisma PascalCase テーブル)。BFF(ai-consultation-assist/bff)は graphql-online-ops.service.ts("v2 appointment queries 用")で online-ops-service を appointmentV2 の取得先にしている。
実証(staging): 患者WEB
/v2で作成した予約1e4d9f39-4aa0-4b8e-9a85-3cc8a6c8650b(accountId 307557 / memberId 307414 / status PENDING / createdAt 2026-08-13 11:37 UTC)はmental_appointmentDB に不在・online_ops_service."Appointment"に存在。同DBにMigrateFixedTimeToTimeSlotLog(固定予約→時間帯予約 移行ログ)テーブルあり=2系統分岐の裏付け。 移行スコープ: 現行予約が載るonline_ops_serviceは既に PG16(16.11)=本移行(#17280 mental_appointment 13.23→16)の対象外。今回の対象はあくまで legacymental_appointment(凍結面の読取+アクティブ面 patients/医師枠の書込)。
全体アーキテクチャ(2つの Aurora = 2系統)
「app(ECS)」と「Aurora(DB)」を分離して表示。v2 の実ストア online_ops_service(既PG16=対象外) と 移行対象 mental_appointment(13.23) の2つの Aurora があり、findById(v2 DB に存在するか)で書込先が決まる。
要点:分岐キーは予約種別ではなく
findById(v2 DB=online_ops_serviceに存在するか)。存在→v2 Aurora で完結/不在→legacy 委譲でmental_appointmentに write。online-ops は「v2 に write/read + mental_appointment を read(固定時間予約)」、mental-appointment-service は「mental_appointment に write/read」。
★どの操作がどっちのDBを触るか(v2/legacy 分岐の実装・早見表)
「6/1」でDBを直接分けているのではない。分岐は3段で、日付分岐は最上流の「空き枠検索」だけ。DBを実際に選ぶのは create=種別/update等=レコード存在(online-ops-service/api/src/appointment/)。
| 段 | 実装 | 分岐キー | DB の決まり方 |
|---|---|---|---|
| ① 空き枠検索 | queries/appointment-availability.facade.ts | businessDate vs CUTOVER_DATE_JST='2026-06-01'(constants/cutover.ts)★唯一の日付分岐 | 枠の種別を決める(<6/1→FIXED_TIME / >=6/1→TIME_SLOT)。DB は選ばない |
| ② 予約作成 | facades/create-appointment.facade.ts L52 | input.appointmentType(種別) | TIME_SLOT→OPS DB(online_ops_service/v2) / FIXED_TIME→Legacy API→mental_appointment |
| ③ 更新/取消/リスケ | facades/{update,cancel,reschedule}-appointment.facade.ts | appointmentRepo.findById()(OPS DBに存在するか) L94/110/147 | 存在→v2(updateTimeSlotAppointmentUsecase) / 不在→Legacy(PATCH /v1/admin/appointments/{id}/patient)→mental_appointment。日付も種別も見ない |
| (read)診察履歴 | queries/patient-examination-history-appointment.read-repository.ts | 無分岐(両DBを読んでマージ) | v2(Appointment)+mental_appointment(appointments L234) を結合。「固定時間予約」行=mental_appointment 由来 |
- 含意:6/1 以降は新規予約が TIME_SLOT で v2 に載るため、以後の create/update も v2 が既定。
mental_appointmentに UI 経由で書くのは 「FIXED_TIME(v2未移行の固定予約)」を更新した時だけ(③の findById 不在パス)。docstring 明記「移行後 全予約が OPS DB 移行完了後、Legacy API 呼び出しは削除」=mental_appointment 書込は過渡期の後方互換。 - ★UI 経由 write を staging で実証(2026-08-14):OPS DB 不在=legacy 確定の固定時間予約
21a32f73-b359-4337-8a90-c342d66c61bc(事前確認in_ops_Appointment=0)を OPSonline-karte.fstdr.app/ops/mental/{id}で開き患者情報(姓)を「てすと」→「てすとテスト」に編集したところ、mental_appointment.appointment_patient_historiesに新規行が追記(count 1→2・recorded_at/updated_at=2026-08-14 02:35・event-sourcing で追記型)。→ UI 経由の mental_appointment write は「legacy(FIXED_TIME・OPS DB 不在)予約の患者情報編集」で可能と確定。分岐は予約単位(findById)なので、その患者が v2 予約を他に持っていても関係ない。 - ★write を ECS まで特定して再実証(2026-08-14・COMMIT タイムスタンプ一致):固定時間予約
09d77b1f-f200-496e-bd88-21117ac6f9db(患者「よねまる いち」)を OPS で開き名(いち→に)を編集。BEFORE=appointment_patient_histories1行(よねまる/いち・05-21 06:32)→ AFTER=2行(よねまる/に・08-14 07:16:03 追記)。同時にpg_stat_activityで10.10.3.65(mental-appointment-service) の直近クエリ=COMMIT@07:16:03 を捕捉=新規行 recorded_at と秒一致。→ 「UI編集→mental-appointment-service ECS が mental_appointment に COMMIT」まで一致で確定(接続・書込・書いた ECS の3点同時実証)。
書込経路の理解図(患者情報編集のシーケンス・実測)
決め手=
findById(OPS DB に在るか)=予約1件単位。日付でも患者でもない。不在(legacy)→mental_appointment に書く/存在(v2)→v2 で完結。
- E2E への効き方:read は「固定時間予約(6/1以前)」の表示(無分岐マージなので mental_appointment 接続が切れると消える)。write は (a) 継続書込監視 + (b) UI(編集可能な legacy 予約を1つブックマークし、Switchover 前後で「編集→appointment_patient_histories に新規行」を確認) の2択。※ legacy 予約は大半が事務局キャンセルのため「編集導線がある legacy 予約」を選ぶ。
移行 E2E への含意(重要)
- ❌ 「新規予約を作成 →
appointmentsテーブルで確認」は誤り(v2 別系統へ行くためmental_appointmentを通らない)。 - ✅ 正しい E2E:
- 凍結面(appointments 等)=読取健全性:consumer(online-ops / Retool / Trocco)が過去予約データを読めることを Switchover 前後で確認。
- ⭐online-ops-service(v2) は mental_appointment の主要な read consumer(Kysely
selectFrom・APPOINTMENT_DATABASE_URL):appointments(14)/appointment_histories(9)/doctors(8)/doctor_appointment_blocks(6)/appointment_cancels(5)/appointment_patient_histories(4)/pick_up_pharmacy_histories(3)/clius_kartes(2)/patients(1)。主コードsrc/appointment/queries/patient-examination-history-appointment.read-repository.ts(患者の過去診察履歴=6/1以前の固定予約を ops/カルテ画面に表示)・appointment.query-service.ts・appointment-block-adapter.ts。 - E2E(UI・staging 実証 2026-08-13):ops/カルテの予約詳細(
online-karte.fstdr.app/ops/mental/{appointmentId})を開き、診察履歴に「固定時間予約」種別の 6/1以前の行(例: 2026-05-31)が表示されることを Switchover 前後で確認。- ★マーカー:診察履歴の
予約種別列で 「固定時間予約」=mental_appointment(appointments) から読んだ legacy 記録 / 「時間帯予約」=v2(online_ops_service)。→ 「固定時間予約(6/1以前)」行が出続ける=mental_appointment read 健全。壊れると 6/1以前履歴が欠落。 - ★実装根拠(
online-ops-service/api/src/appointment/queries/patient-examination-history-appointment.read-repository.ts):診察履歴クエリpatientExaminationHistoriesは2ソースをマージ。時間帯予約=this.kysely.selectFrom('Appointment')(online_ops_service/v2)/固定時間予約=this.kyselyAppointment.replica(true).selectFrom('appointments')(L227/234・=mental_appointment)。kyselyAppointmentはAPPOINTMENT_DATABASE_URL(=mental_appointment) 接続で、if (!this.kyselyAppointment.isConnected()) return [](L220)ガードあり=mental_appointment に繋がらなければ固定時間予約行は空になり画面から消える。∴ 固定時間予約行の表示=mental_appointment read 成功の証跡。 - 準備:6/1以前に「固定時間予約」を持つ患者を1人ブックマーク。
- ★推奨ブックマーク(staging・2026-08-14 DB裏取り済):
https://online-karte.fstdr.app/ops/mental/09d77b1f-f200-496e-bd88-21117ac6f9db- 患者「よねまる いち」(accountId 307191 / memberId 306877)。メイン予約
09d77b1f自体が固定時間予約(2026-05-31 07:30・事務局キャンセル)=URL を開くだけで online-ops がmental_appointment.appointmentsを read。 - Legacy DB 実在確認:
appointments.id=09d77b1f…(patient_idb208a823・MENTAL_HEALTH)/この患者は Legacy に固定予約3件。 - 画面での確認箇所:予約情報の「予約種別=固定時間予約」表示(診察履歴の「予約種別」列で「固定時間予約」行でも可)。表示が消えたら mental_appointment read が壊れたサイン。
- 患者「よねまる いち」(accountId 307191 / memberId 306877)。メイン予約
- (旧例)
ops/mental/e7e90236-06b5-4a71-b274-e10b4c71ee32(2026-05-31 固定時間予約あり・確認済)。 - ⚠️ ops URL の id は mental_appointment の
appointments.id/patients.idとは限らない(v2 側 id のことがある)。固定時間予約の予約IDは Legacyappointments.idと一致(09d77b1f で実証)。
- ★推奨ブックマーク(staging・2026-08-14 DB裏取り済):
- ★マーカー:診察履歴の
- ⭐online-ops-service(v2) は mental_appointment の主要な read consumer(Kysely
- アクティブ面(
patients/doctor_appointment_blocks/appointment_patient_histories)=書込健全性:書き手=mental-appointment-service(repochronic-phase-appointment-service・現役デプロイ) と特定。Switchover 後に以下の書込が通ること:- ★アクティブ面 write テスト用の FIX 案件(staging・2026-08-14 実証):
09d77b1f-f200-496e-bd88-21117ac6f9db(患者「よねまる いち」・memberId 306877)。OPS でこの legacy 予約の患者情報(氏名)を編集→mental_appointment.appointment_patient_historiesに新規行追記(実証: 名 いち→に で 1→2 行・10.10.3.65(mental-appointment-service) のCOMMIT@07:16:03 と秒一致)。Switchover 前後で同編集を行い追記されることを確認する。 - ★テスト可否(アクティブ3表・個別に明記):
テーブル UI 固定テスト案件 方法 appointment_patient_histories✅ 09d77b1f(よねまる いち)legacy 予約の患者情報編集→行追記(実証済) patients△ 同案件( 09d77b1f)の患者情報編集の patient master 更新経路実証マーカーは appointment_patient_historiesを推奨(確実に追記)doctor_appointment_blocks❌ UI 固定案件は無い(UI からはテスト不可) 現行 UI の医師枠操作は v2( createDoctorShiftBlockV2→online_ops_service)に書き、mental_appointment.doctor_appointment_blocksへは UI から直接書けない(書込は legacy/バッチ経路)。→ 継続書込監視(max(updated_at)/n_tup_insの進行)で健全性確認する patients:src/appointment/repository/patient.repository.ts(patient.create/update)。トリガ=管理 usecaseupdatePatient・sendKarte(ORCA患者ID紐付け)・予約フロー。appointment_patient_histories:AppointmentPatientHistory(患者情報スナップショット)。doctor_appointment_blocks:doctorAppointmentBlock.repository.ts(createMany・医師ブロック設定)。- ※ online-ops-service(v2) は mental_appointment の live 3表を直接書かない(v2 の Kysely 書込は自前
online_ops_serviceの PascalCase 表。mental_appointment へはappointment-block-adapterで読取のみ)。医師シフト(稼働)は別サービスdr-shift(DR_SHIFT_SERVICE_ENDPOINT・/entried-shifts)を両者が HTTP 参照。 - ★書込 E2E の方式(staging 実測+実装で確定)=「継続書込監視」+「legacy 予約編集の UI」の2択:
- v2 予約(6/1以降)への UI 操作は全て v2(online_ops_service) に書く(実測3操作):
- 患者情報変更(氏名/カナ)(v2予約 加藤さん)→ v2
AppointmentPatientのみ更新(mental_appointment.patients.updated_at不変)=実測。 - ORCA 登録/再確認→ 空振り(実保険証+OCR→オン資→ORCA・外部副作用大)。
- 医師ブロック→ v2 mutation
createDoctorShiftBlockV2(/doctor-shifts)=v2 書込。
- 患者情報変更(氏名/カナ)(v2予約 加藤さん)→ v2
- ✅ ただし「legacy(FIXED_TIME・OPS DB 不在)予約」の患者情報編集は mental_appointment に書く(2026-08-14 実証):予約
21a32f73…(in_ops_Appointment=0)を OPS で姓「てすと」→「てすとテスト」に編集 →appointment_patient_historiesに新規行追記(count 1→2・02:35・event-sourcing)。分岐は予約単位(findById)=③の legacy パス。 - ★実装根拠(
online-ops-service/api/src/appointment/facades/update-appointment.facade.ts):予約更新は Facade が 「OPS DB(=online_ops_service/v2) に予約が存在するか」でルーティング。appointmentRepo.findById()がヒット(=TIME_SLOT/v2 予約)→updateTimeSlotAppointmentUsecase→ online_ops_service に書く(L110-114)。その後 会員基盤/ORCA へ同期(L122-142)。- 不在(=v2未移行の FIXED_TIME 固定予約)→
updateViaLegacyApi→PATCH /v1/admin/appointments/{id}/patient→ mental-appointment-service →mental_appointment(L147-150, L301)。 - docstring 明記:「移行後:全予約が OPS DB に移行完了後、レガシー API 呼び出しロジックは削除される」=mental_appointment 書込は過渡期の後方互換。設計 doc も「write は v2 一本/V2_FLAG 本番常時 ON」。
- → mental_appointment に UI から書く唯一の道=「固定時間予約(v2未移行 legacy)」を更新することだが、legacy 予約は 6/1 以降作られず大半が事務局キャンセルで編集導線が限られる。
- ∴ 書込 E2E = Switchover 前後で
patients/doctor_appointment_blocks/appointment_patient_historiesのmax(updated_at)/n_tup_insが引き続き進むかを read-only 監視(本番は自然発生・staging は必要なら backend 同期を意図的にトリガ)。書き手(mental-appointment-service)と経路はコードで確定済み。
- v2 予約(6/1以降)への UI 操作は全て v2(online_ops_service) に書く(実測3操作):
- ★アクティブ面 write テスト用の FIX 案件(staging・2026-08-14 実証):
- 件数突合は全テーブルで実施(凍結面は不変、アクティブ面は微増を許容)。
- 凍結面(appointments 等)=読取健全性:consumer(online-ops / Retool / Trocco)が過去予約データを読めることを Switchover 前後で確認。
- ⚠️ 移行は依然必要(
patients/doctor_appointment_blocksが当日も書込=DB は生きている)。ただし E2E は上記に沿う。
★本番トラフィック実測:read/write の実在+consumer別(CloudWatch+pg_stat・2026-08-14)
コードで立てた「ECS×read/write マトリクス」を本番の実トラフィックで裏取りした(read-only・踏み台 i-09384db1d690bcfd0→Writer/Reader 各インスタンスへ SSM port-forward・pg_stat_* 参照のみ)。
① CloudWatch(Datadog aws.rds.*・直近3h・Writer=mental-appointment-0/Reader=mental-appointment-1)
| 指標 | Writer | Reader | 読み |
|---|---|---|---|
| database_connections | 25 | 8 | 両endpointに consumer 接続中 |
| write_iops / write_throughput | 1,221 / ~201KB/s | 0 / 0 | Writer で書込・Reader ゼロ(正) |
| commit_throughput | 446/s | 623/s | tx commit 発生 |
| read_iops / read_throughput | 0 / 0 | 0 / 0 | ⚠️ ディスク読み=0。「読まれてない」ではなく buffer cache 全載り(下の blks_hit 参照)。CloudWatch read系ではアプリ SELECT を判定不可 |
② pg_stat(5秒デルタ=ライブか)
Reader mental-appointment-1 | Writer mental-appointment-0 | |
|---|---|---|
| Δtup_returned(読取) | 1,613,838 行(≈32万行/s) | 1,987 |
| Δins/upd/del(書込) | 0 / 0 / 0(reader は書かない) | 0(書込はバースト的) |
| 累積 DML(ins/upd) | 2.6M / 3.8M | 122K / 28K(書込実績あり) |
| blks_hit / blks_read | 1.5兆 / 20.5万(cache hit 99.99%+) | 97.8億 / 4.4万 |
→ READ は大量にライブ(Reader Δtup_returned=1.6M/5s)。CloudWatch read_iops=0 の正体=全読取が buffer cache hit でディスクに落ちないため。∴ 読取健全性は read_iops ではなく「接続数/DBLoad/PI/pg_stat tup_returned」で見る。
③ consumer 別の切り分け(pg_stat_activity.client_addr の CIDR が決め手)
| client_addr | ネットワーク | consumer | 接続先 | R/W |
|---|---|---|---|---|
| 10.10.x.x(例 10.10.22.166 他多数) | ap-ne-1 peered(10.10.0.0/20・fd-platform-vpc) | online-ops-service | Reader | read(1.6M tup/5s の主) |
| 10.0.x.x(例 10.0.2.112 / 10.0.3.201) | us-east-1 local(mental-appointment-sg) | mental-appointment-service | Writer | write(Prisma・DML) |
| local / rdsadmin(JDBC) | — | RDS内部・PI/監視 | 両方 | —(Retool/Trocco は間欠でこの瞬間は非接続) |
∴ 本番実測での確定マトリクス
| ECS(driver) | READ mental_appointment | WRITE mental_appointment | 本番証跡 |
|---|---|---|---|
| mental-appointment-service(Prisma 5.13) | ✅ | ✅ | Writer に 10.0.x・DML 実績 |
| online-ops-service(Kysely+pg) | ✅(Reader・32万行/s) | ❌(Reader Δins/upd/del=0) | Reader に 10.10.x/Writer にも 10.10.x あり(下記 ⚠️) |
⚠️ 訂正(2026-08-26 本番実測):ここに元々あった「Writer に 10.10.x 不在」は 点観測での取り逃しだった。 実測では Writer 側にも online-ops の接続が存在する。内訳は Reader = Kysely の
select "appointments"…(固定時間予約の read)/ Writer = deep-health のSELECT 1(kyselyAppointment.ping()は replica と primary の両プールにSELECT 1を投げる・#17624)。 どちらもonline-ops-service-serviceのタスク(SGonline-ops-service-sg)から来ていることを、 ENI の SG + 稼働タスクのprivateIPv4Address突合で確定済み。 write が無いという結論は変わらない(Writer 上の 10.10.x はSELECT 1のみ・Δins/upd/del=0)。 → §3 reboot 後・Switchover 後の再接続確認は、Reader だけでなく Writer 側も見ること。
再現手順(read-only):
i-09384db1d690bcfd0から Writer/Reader インスタンス endpoint へ SSM port-forward →psqlでSET default_transaction_read_only=onの上、pg_stat_database(tup_returned/inserted…の5秒デルタ)とpg_stat_activity(client_addr 別)を参照。consumer 別は client_addr の CIDR(10.10.x=online-ops / 10.0.x=mental-appointment-service)で割る。Switchover 前後で同手順=read(Reader Δtup_returned>0)・write(Writer DML/接続) の生存を確認できる。 ⚠️ online-ops の read 接続は idle 約10秒で回収されるため、点で見ると「不在」に見える。OPS UI を操作しながら連続実行して捕捉すること(上記の取り逃しの原因)。CIDR による推定だけで判断せず、ENI の SG + ECS 稼働タスクの privateIPv4 突合まで行うと確実(手順はproduction.md§8-C の代替手段)。
★staging:各 ECS の接続・処理を live 実測(ECS×read/write を UI 操作で捕捉・2026-08-14)
「UI 操作が該当 ECS から staging Aurora に対して行われたか」を、pg_stat_activity.client_addr を ECS タスク IP に逆引きして証明した(踏み台 i-0ddd2353e102d9006・read-only)。staging は ap-ne-1・単一インスタンス(mental-appointment-0.ctb7v7xkimcr…)。
IP → ECS 対応(staging・ECS describe-tasks で確定)
| ECS タスク IP | ECS サービス | task SG | DB SG inbound |
|---|---|---|---|
10.10.3.65 | mental-appointment-service(writer 本体・Prisma) | sg-01f3935c7fba1da5c | ✅ 許可 |
10.10.25.165 | online-ops-service-service(API・Kysely) | sg-0d46c19ecedbcb45c | ✅ 許可 |
live 実測結果
| ECS | READ | WRITE | staging 証跡 |
|---|---|---|---|
mental-appointment-service(10.10.3.65) | ✅ | ✅ | SELECT … appointments(live)+Aurora データ更新:appointment_patient_histories(21a32f73) が「てすと」(6/1) → UI 編集で 「てすとテスト」(08-14 02:35:21) を追記=1→2 行 |
online-ops-service(10.10.25.165) | ✅ | (書込は v2・mental_appointment には書かない) | UI で固定時間予約患者ページをリロードした瞬間、10.10.25.165 から SELECT "appointments"…(Legacy DB read)を捕捉(04:11:46) |
- 固定時間予約の read が Legacy DB(mental_appointment) 由来である実装根拠:
patient-examination-history-appointment.read-repository.tsはkyselyAppointment.replica(true).selectFrom('appointments')(L227/234・コメント L183「固定予約は OPS DB ではなく Legacy DB の appointments テーブルにのみ存在する」)。isConnected()ガードは pool 初期化成功で true(node-postgres は遅延接続)。 - 裏付け:Datadog に online-ops-service
Appointment database pools initialized successfully(エラー無し)。configAPPOINTMENT_DATABASE_URL/APPOINTMENT_REPLICA_DATABASE_URL→ この staging クラスタ(writer/cluster-ro)。 - ⚠️ 捕捉の注意(手順に必須):online-ops の read 接続は pg の idle 接続が約10秒で回収されるため、単発スナップショットでは写らない。UI 操作中に 1〜2秒間隔で連続サンプリングして初めて
SELECT appointmentsを捕捉できる(本実測もそれで捕捉)。Switchover 検証時も同様に「操作しながら連続 poll」で確認する。 - ∴ staging でも本番同様、mental-appointment-service(read+write) と online-ops-service(read) の両 ECS が Aurora に接続・処理していることを live で実証済み。
1. 環境・接続
| 項目 | production |
|---|---|
| AWS プロファイル | production-admin(⚠️ 書込権限あり・preflight は read-only 徹底) |
| リージョン(DB) | us-east-1(Aurora Global の primary。cluster endpoint も us-east-1) |
| クラスタ識別子 | mental-appointment |
| cluster endpoint | mental-appointment.cluster-cxmwyphil4em.us-east-1.rds.amazonaws.com |
| Secrets Manager Secret ID | mental-appointment(⚠️ 値に制御文字を含む。§付録の strict=False パース必須) |
| DB 名 | mental_appointment |
| 踏み台(bastion) | i-09384db1d690bcfd0(fd-platform-bastion・us-east-1) ← DB SG が fd-platform SG を許可済み |
| DB SG | sg-02088474cf99248f4(fd-platform sg-09a3dec4eb92eebf0 を 5432 で許可) |
| アプリ | NestJS ^10.3.8 + Prisma 5.13.0 + GraphQL Yoga federation + Node 20=PG16 対応◎。source: fastdoctor-jp/chronic-phase-appointment-service(package name = mental-appointment-service・ECR mental-appointment)。生SQL 33件も PG16 安全(§4)。※ online-ops-service(mental_appointment へは Kysely+pg=kyselyAppointment)は同DBを読む別 consumer(§3/§9) |
2. preflight 実測結果(read-only・本番 mental-appointment・2026-08-13 実測)
skill pg16-preflight 実行結果(権威データ):docs/sre/scripts/pg16-preflight-check.sh --target mental-appointment --profile production-admin --region us-east-1 --allow-production
SUMMARY: PASS=23 WARN=4 FAIL=0 → ブロッカーなし
MATRIX : mental-appointment | kind:aurora | engine:aurora-postgresql | version:13.23
| logical:off | reader:yes | global:yes | pgaudit:no | spl:pg_stat_statements
| max_wal_senders:20 | max_replication_slots:20 | publications:0
| high_ext:none | warn_ext:none | app:要(repo) | risk:medium
| blocker:none | next:reader-plan,custom-cpg,app-grepWARN(4): ①production 接続(想定内)②members=2=Reader あり(再起動/AutoScaling 考慮)③Global Database(Global削除→標準BG)④logical=off/wal_sender_timeout=1min(CPG 付替+再起動)。他は全 PASS(PKなし表0/長時間tx0/reg型0/FDW0/postgis・pg_repack なし 等)。
以下は踏み台経由 psql の補足(§付録の手順・書き込み一切なし)。
| 項目 | 実測値 | 判定 | メモ |
|---|---|---|---|
server_version | 13.23 | — | 現行。target = 16.14(§9-A・現状最新の有効 BG ターゲット。16.13 も可) |
rds.logical_replication | off | ✅ | 論理レプリ未使用 |
wal_sender_timeout | 1min(既定) | — | CPG 方針次第(dr-work-record は 0 設定) |
password_encryption | md5 | ✅ | ⭐重要:PG16 でも CPG が md5 なら md5 維持(scram 強制なし)。JDBC/Prisma とも md5 で継続接続可 → 認証方式起因のアプリ断はなし |
shared_preload_libraries | rdsutils,rds_casts,pg_stat_statements | ⚠️ | pgaudit なし=default CPG。→ custom CPG 作成が必要(#17388/#17389) |
max_connections | 1706 | ✅ | 余裕大 |
| current connections | 38 | ✅ | app pool(idle)26 + JDBC 4 + その他 |
| longest tx(ユーザー) | 0 | ✅ | ⭐訂正:skill A-3 長時間tx=0。当初手動クエリで «≈8.4日» と出たのは pg_stat_activity 全体(walsender 等バックグラウンド backend の xact_start)を拾った誤検知。実ユーザーの長時間 tx はなし |
| reader(members) | 2(Writer+Reader) | ⚠️ | 再起動は Reader→Writer 順。AutoScaling 考慮 |
pg_replication_slots | 0 | ✅ | 常設 CDC 消費者なし |
pg_publication | 0 | ✅ | logical publication なし |
pg_subscription | 0 | ✅ | subscriber なし |
pg_prepared_xacts | 0 | ✅ | 2PC 未使用 |
| db_size | ≈1,390 MB(1.4GB) | — | dr-work-record より大きい |
接続元アプリ(application_name 集計):(空) 26 idle(Prisma app pool=本体/online-ops)/ PostgreSQL JDBC Driver 4(Retool・§3 で特定済)/ psql 1(調査)。
3. consumer 棚卸し(DB SG sg-02088474cf99248f4 の 5432 inbound)※確定版(2026-08-13)
DB SG inbound(5432)の source 全量:mental-appointment-sg(sg-0bc93fb524e298f7d) / fd-platform-retool(sg-024766cf722c3329a) / fd-platform(sg-09a3dec4eb92eebf0) + CIDR 10.10.16.0/20・10.10.0.0/20(=fd-platform-vpc 10.10.0.0/16・ap-northeast-1 のクロスリージョン peering)。
接続している アプリ / SaaS(確定)
| 種別 | 実体 | 接続 | 経路(許可元) | 裏取り |
|---|---|---|---|---|
| 🟦 アプリ①(本体) | mental-appointment-service(repo chronic-phase-appointment-service・Prisma 5.13.0・GraphQL Yoga federation) | writer | mental-appointment-sg | ECS→ECR→repo→現接続ENI(実測) |
| 🟦 アプリ②(ops) | online-ops-service/api(mental_appointment へは Kysely 0.27.3 + pg 8.11.5=kyselyAppointment。※自DB online_ops_service は別スタック) | reader(read専用)※ | fd-platform-vpc CIDR(ap-ne-1→us-east-1 peering) | secret online-ops-service の APPOINTMENT_DATABASE_URL/REPLICA 実測 |
| 🟧 SaaS③ | Retool(運用/管理画面・JDBC) | — | fd-platform-retool SG(ENI 1・pg_stat_activity JDBC ×4) | DB SG + 実接続 |
| 🟧 SaaS④ | Trocco(ETL/データ連携・バッチ抽出) | reader cluster-ro- | fd-platform-vpc 経由 | projects/trocco/connections/postgresql/mental-appointment-prd.json |
| ⬜ 管理 | fd-platform 踏み台 | — | fd-platform SG | 本 preflight で使用 |
※ online-ops-service は read 専用(2026-08-14 本番実測で確定・§0「本番トラフィック実測」)。config(
APPOINTMENT_DATABASE_URL=writer /APPOINTMENT_REPLICA_DATABASE_URL=cluster-ro) は writer/reader 両 endpoint を持つが、pg_stat_activity の client_addr で切り分けた結果、online-ops の接続は Reader のみ・Writer に 10.10.x 不在・Reader の Δins/upd/del=0=mental_appointment には書かない。コード上もkyselyAppointmentはselectFromのみで insert/update/delete 0件。→ Switchover の write 影響評価では online-ops は reader 扱い(write consumer は mental-appointment-service のみ)。
接続していない(誤検出を排除)
- ai-triage / dr-shift / payment-service … 非接続で確定(4系統検証):①task-def 平文 host=各自クラスタのみ ②
DATABASE_URL(Secret 由来)を解決=ai_triage/shift_production/payment_service(mental-appointment 参照ゼロ)③DB SG 許可リストに 3者の SG 不在 ④実DB 現接続はmental-appointment-sgENI のみ。- ※ 共通接尾辞
cxmwyphil4emはアカウント/リージョン共通の endpoint ハッシュでクラスタ名(プレフィックス)が別=別クラスタ。文字列一致による誤検出だった。
- ※ 共通接尾辞
- 論理CDC 系 SaaS(Datastream 等)… 無し:
pg_publication=0/replication_slot=0/subscription=0。Trocco は reader への query ベース バッチ抽出(論理レプリ不使用)で BG の external replication 阻害要因にならない ✅。
ネットワーク補足(NAT 経由か?)
このDBは private subnet・SG制限・パブリックアクセス無効。到達は常にプライベート経路(同一VPC直/クロスリージョン VPC peering・TGW)で、NAT Gateway 経由ではない(NAT は private→インターネットの egress 用で、非公開 RDS への到達経路ではない)。CIDR 10.10.0.0/16 許可=fd-platform-vpc(ap-ne-1) からの peering。
Switchover 影響を受ける接続先= ①②アプリ + ③Retool + ④Trocco。移行後は 4者すべて疎通再確認(特に ②④ は reader endpoint 依存)。両リージョン(us-east-1 本体 / ap-ne-1 ECS・ALB)あり、BG/Switchover 対象は writer=us-east-1。
判定:replication_slots=0 / publication=0 / subscription=0 により 常設 CDC 消費者なし ✅。移行の論理レプリ観点はクリーン。
3-b. preflight オブジェクト包括確認(本番 mental_appointment・2026-08-13 実測・全PASS)
skill A-1/A-2 系に加え、外部キー整合・カスタム型・collation 等を追加確認。全てクリア。
| 項目 | 結果 | 判定 |
|---|---|---|
| large objects / unlogged table / materialized view | 0 / 0 / 0 | ✅ |
| view | 2(定義に EXTRACT/階乗なし=skill C-2) | ✅ |
| foreign table(FDW) / partitioned / 継承 | 0 / 0 / 0 | ✅ |
| sequence | 0(全 UUID PK) | ✅ BG のシーケンス drift 問題が原理的に発生しない |
| 外部キー制約 | 16(全 VALID・NOT VALID=0) | ✅ |
CHECK 制約 NOT VALID | 0 | ✅ |
enum 型(MEDICAL_SERVICE_TYPE 等) | 9 | ✅ major upgrade で保持・PG16 安全 |
| domain / composite 型(user) | 0 / 0 | ✅ |
| user トリガ / event トリガ | 0 / 0 | ✅ |
| PK 無しテーブル / 非default REPLICA IDENTITY | 0 / 0 | ✅(§2・BG 複製クリーン) |
| collation | en_US.UTF-8・非default 列 collation=0 | ✅(下記) |
| 拡張 | plpgsql 1.0 のみ | ✅ |
sequence=0:全表 UUID PK のため BG 切替時の「Green でシーケンス未進行」問題(メジャー移行の定番リスク)が発生しない。 collation:一般のメジャー移行では glibc collation 変化で text index の REINDEX が論点だが、Aurora BG は Green を新 PG16 で構築し論理レプリでデータ再投入+index 再作成するため collation 破損は非該当(Aurora が glibc 管理)。 外部キー16本 全VALID:整合クリーン。Aurora BG が Green 側で FK 再構築+整合適用。
4. アプリ/フレームワーク互換性
対象 repo(ALB mental-appointment の本体):fastdoctor-jp/chronic-phase-appointment-service(package name = mental-appointment-service・ECR mental-appointment を release-production.yml で push・ECS mental-appointment-cluster/mental-appointment-service)。DATABASE_URL で mental_appointment に直結。
| 項目 | 値 | PG16 互換 |
|---|---|---|
| フレームワーク | NestJS ^10.3.8 | ✅ |
| ORM | Prisma 5.13.0(@prisma/client 5.13.0 / prisma ^5.13.0) | ✅(Prisma 5.0+ で PG16 正式サポート) |
| GraphQL | GraphQL Yoga federation subgraph(@graphql-yoga/nestjs-federation ^3.10.6) | ✅ |
| ランタイム | Node >=20 | ✅ |
| 認証方式 | DB password_encryption=md5(§2) | ✅ md5 継続で断なし |
生SQL grep 結果(src・テスト除外・33件・appointment/repository/appointment.repository.ts に集中)=PG16 非互換パターン 0:
| 使用構文(頻度) | PG16 判定 |
|---|---|
AT TIME ZONE(27) | ✅ 標準 |
LATERAL join(9) | ✅ 標準 |
${id}::uuid cast(6)/ enum cast ::"MEDICAL_SERVICE_TYPE"(2) | ✅ 標準 |
Prisma.join(4)/ window OVER()(1) | ✅ 標準 |
$queryRaw+Prisma.sqlを多用(33件)だが、削除機能(EXTRACT特殊/reg*型/abstime/pg_start_backup等)は 0。すべて PG16 で有効な構文。 ⚠️ ただし生SQL 33件と密結合のため、移行後の E2E(§5 ③ GraphQL 経由)で予約系クエリの回帰確認を推奨。
⚠️ 別 consumer(第2の DB 利用者・接続確認済):mental-online-karte projects/online-ops-service/api(@fastdoctor-jp/online-ops-service)も同じ mental_appointment DB を広範に読む(ops 用途・§0/§3)。secret online-ops-service の APPOINTMENT_DATABASE_URL が mental-appointment.cluster-...(writer)+ cluster-ro-...(reader)を指すことを実測確認。Switchover 影響あり→移行後 疎通再確認必須。
★ consumer 側ドライバ/クエリビルダの PG16 互換(read 経路も評価済)
| 役割 | サービス | スタック | PG16 互換 | 根拠 |
|---|---|---|---|---|
| 書き手 | mental-appointment-service | Prisma 5.13.0(+ node20 / GraphQL Yoga) | ✅ | Prisma 5.0+ で正式サポート |
| 読み手 | online-ops-service | Kysely 0.27.3 + pg(node-postgres) 8.11.5(PostgresDialect+Pool・primary/replica) | ✅ | ①Kysely は SQL ビルダーで互換はドライバ依存 ②pg 8.x は PG16 対応 ③online-ops 自前DB online_ops_service は既に Aurora 16.11 で同一スタック稼働=本番実証済 |
| 読み手 | Retool / Trocco | PostgreSQL JDBC | ✅ | JDBC は PG16 対応 |
Kysely 補足:Kysely 自体はバージョン非依存。型パーサのカスタマイズは
types.setTypeParser(INT8→Number)のみで、INT8(OID 20) は全 PG 共通 builtin=PG16 で OID 変化なし・無害。生 SQL(sql\``)による PG 固有機能の危険使用も無し(read クエリは selectFrom/join 中心)。→ mental_appointment 13→16 で online-ops の read に新規互換リスクなし。
結論:アプリ側コード変更なしで PG16 対応可(◎)。書き手 Prisma 5.13.0・生SQL は PG16 安全構文のみ・読み手 Kysely+pg 8.11.5 は既に PG16(16.11) 実績・md5 認証維持で接続断なし。
5. E2E 疎通確認方法(3経路)
| 経路 | 内容 | 前提 | 状態 |
|---|---|---|---|
| ① 踏み台→DB 直(psql) | server_version / 設定値 / 全テーブル件数(§7) | DB SG が fd-platform 許可(済) | ✅ 実施可(本 preflight で実演済) |
② 踏み台→ALB→app /health | app 稼働+ルーティング。⚠️ @nestjs/terminus check([]) で DB を含まない | 踏み台→ALB(80) SG | ⚠️ SG 適用後に可(DB は触らない) |
③ 踏み台→ALB→app→DB /graphql | DB を読む GraphQL query でアプリ→DB まで疎通 | 踏み台→ALB(80) SG + read query 選定 | ⚠️ SG 適用後に可(full path 検証はこれ) |
- ② / ③ には「踏み台→ALB(80)」SG が必要(staging = terraform_for_aws#2621。本番は同等 PR を別途作成予定)。
/healthだけでは app→DB を確認できない(check 配列が空)。full path は ③ の DB 読み取り GraphQL query で確認する。- 画面経由 E2E:予約画面(患者/管理)から予約作成・取得。テスト予約データ作成手順は残タスク(staging は
saku:create-test-receptionスキルあり・本番は不使用)。
app 側 ALB(②③の宛先・writer リージョン us-east-1):internal-mental-appointment-1464731835.us-east-1.elb.amazonaws.com
# ② /health(DBは触らない)
curl -s "http://<ALB>/health" -w ' [%{http_code}]'
# ③ /graphql(DBを読む=full path。安全な read query をスキーマから選定)
curl -s -XPOST "http://<ALB>/graphql" -H 'content-type: application/json' \
-d '{"query":"<DBを読むクエリ>"}' -w ' [%{http_code}]'6. CPG 設計方針(#17388 / #17389)
⚠️ 6-0. 現状 CPG の環境差(重要・#17388/#17389 の前提)
staging と本番 us-east-1 で現状の CPG 状態が異なる(2026-08-13 実測):
| 環境 | クラスタ | 現状 Cluster PG | logical / pgaudit |
|---|---|---|---|
| staging | ap-ne-1 単一 | custom mental-appointment(PG13 family) | on / あり |
| 本番 東京 | ap-ne-1 | custom(main_tokyo.tf で適用) | on / あり |
| 本番 us-east-1(★移行対象) | Global primary | default.aurora-postgresql13 | off / なし |
- 原因:custom CPG(
rds.logical_replication=1/pgaudit.log=all/wal_sender_timeout=0/max_wal_senders=20)はfastdoctor-template/pf-mental-appointment-serviceのvariable.tfに定義され、staging/main.tf+production/main_tokyo.tf(東京)で適用。production/main.tf(us-east-1)には CPG 設定が無い。staging main.tf に TODO コメント# バージニア側で作成したものをモジュールに取り込む= us-east-1 は Global DB 構築時に別途作成され custom CPG 未適用・IaC 未完全 import。 - 含意:
- staging(#17388):source 側は既に logical=on(PG13 custom CPG)。→「CPG付替+reboot で logical 有効化」は実質不要。作業は PG16 family の custom CPG(Green/target 用)を作るだけ。既存 PG13 custom CPG の custom parameter を PG16 側に揃える。
- 本番(#17389):us-east-1 は default(logical=off)。→ 手順書の「CPG付替+reboot」が本番の必須実ステップ(reboot 伴う)。本番用 PG16 custom CPG 作成+付替で logical 有効化。
- 技術的負債:本番 us-east-1 は custom CPG 未適用=IaC 未完全管理。今回の CPG付替が custom 管理下へ入れる好機(付替後に import/整合)。
- ★インスタンス側 DB parameter group も存在(見落とし注意):cluster PG は
default.aurora-postgresql13だが、インスタンスmental-appointment-0/-1には instance DB PGmental-appointmentが適用済(user 変更パラメータ 0件=実質 default で実害なし)。→ PG16 化では cluster PG に加え instance PG(PG16 family) も必要。標準 BG の--target-db-parameter-group-name(instance PG)に PG16 版を指定する。未作成だと当日「instance PG が無い」で詰まるため #17388/#17389 の CPG 作成に instance PG(PG16) も含める。
6-1. 目標 CPG パラメータ(PG16・両環境共通の設計値)
現状 本番 us-east-1 は default CPG(pgaudit なし・logical off) のため、dr-work-record と同方針で custom CPG を作成する(staging は既存 PG13 custom を PG16 family に引き継ぐ)。
| パラメータ | 値 | 理由 |
|---|---|---|
shared_preload_libraries | pgaudit,pg_stat_statements | pgaudit セッション監査 + 統計 |
pgaudit.log | all | 監査ログ有効化する |
rds.logical_replication | 1 | BG レプリ用(dr-work-record 準拠) |
wal_sender_timeout | 0 | BG レプリ切断防止 |
max_wal_senders | 20 | 既存 staging/東京 custom に合わせる |
max_slot_wal_keep_size | 1000 | slot WAL 保持 |
※
max_wal_sendersの現状値の内訳:§2 SUMMARY のmax_wal_senders:20は DB 実行時値(current_setting=Aurora の実効デフォルト)。一方default.aurora-postgresql13のパラメータ定義値は 10。本番 us-east-1 は default CPG のため「定義=10 / 実行時=20」の差がある。custom CPG で 明示的に 20 に設定して定義値と実行時値を一致させる(max_replication_slotsも同様に 20)。
- pgaudit は
shared_preload_libraries+ reboot で有効化(CREATE EXTENSION不要・session 監査)。pg_stat_statementsはCREATE EXTENSION必要。 password_encryptionは md5 維持(scram 強制しない)=アプリ断回避。
7. 移行前ベースライン(全16テーブル件数・2026-08-13 実測)
Switchover 後に同一 query で件数照合する(pg16-verify-*.sh の 2b/2c 相当)。
| テーブル | 件数 | テーブル | 件数 | |
|---|---|---|---|---|
| appointment_histories | 533,521 | appointment_patient_histories | 273,671 | |
| appointment_health_insurance_histories | 336,821 | appointments | 268,015 | |
| pick_up_pharmacy_histories | 292,800 | clius_kartes | 236,116 | |
| appointment_online_videos | 236,177 | appointment_required_actions | 100,147 | |
| patients | 69,389 | appointment_cancels | 65,211 | |
| doctor_appointment_blocks | 27,847 | image_files | 8,932 | |
| appointment_medical_subsidy_certificate_histories | 770 | doctors | 622 | |
| _prisma_migrations | 27 | appointment_email_verifications | 0 |
dr-work-record より大きい(最大 53万行・計 ~1.4GB)。exact
count(*)は動作確認済だが照合時間は長め(必要ならn_live_tup近似へ切替)。
8. 所見・要対応・残タスク
難易度:中(medium)(skill risk:medium)。根拠=Reader あり+Global DB+CPG 付替(dr-work-record と同型)。FAIL=0・ブロッカーなし。app 面は grep 済でクリア(生SQL 標準のみ・書き手 mental-appointment-service=Prisma 5.13.0/読み手 online-ops=Kysely+pg)だが、**中の主因はインフラ(Reader/Global)**で app ではない。
✅ クリア:FAIL=0(skill)/ logical=off・slot/publication/subscription/2PC=0(CDC 消費者なし)/ 長時間 tx=0(8.4日は誤検知・訂正済)/ md5 認証維持でアプリ断なし / 本体 mental-appointment-service(Prisma 5.13.0)・生SQL 33件も PG16 安全構文のみで互換 ◎ / 踏み台→DB 直アクセス可。
⚠️ 要対応(next: reader-plan / custom-cpg / app-grep):
- custom CPG 作成(pgaudit 込み・#17388/#17389)。default のため必須。
- Reader→Writer 順の再起動計画(members=2・AutoScaling 考慮)。
- 踏み台→ALB(80) SG(本番=#17398・staging=#17394/#2621)を作成し ②③ を検証。
✅ 解決済(#17281):
- Retool 接続元=
fd-platform-retoolSG(JDBC)と特定(§3)。 - CIDR
10.10.16.0/20/10.10.0.0/20の実体=fd-platform-vpc(10.10.0.0/16・ap-northeast-1) のクロスリージョン peering と特定(§3)。 - consumer 確定:アプリ①mental-appointment-service ②online-ops-service + SaaS③Retool ④Trocco + 踏み台。ai-triage/dr-shift/payment は非接続で確定。
✅ 解決済(#17281・§9 参照):target 16.x 確定 / federation gateway 特定+③read query / 東京 consumer 移行後再確認手順 / テスト予約データ作成手順。
□ 残タスク(#17281):(上記4件は §9 で解決。以降は移行実施フェーズで対応)
- staging/本番 手順書本体(#17390/#17391/#17392)への反映。
9. 移行後検証・E2E 詳細手順(#17281 残タスク回答)
9-A. target engine version(16.x)確定
aws rds describe-db-engine-versions --engine aurora-postgresql --engine-version 13.23 の ValidUpgradeTarget(us-east-1・2026-08-14 再実測):
| 選択肢 | 状態 |
|---|---|
| 16.14 | ✅ 採用推奨(現状の最新 available な有効 BG ターゲット・status=available) |
| 16.13 | 利用可(available)。dr-work-record 実績=13.23→16.13 |
| 16.11 | 可(旧) |
| 16.15 | Aurora(us-east-1) に未提供(None) |
→ target = 16.14。マイナー差=bug/security 修正の累積のみ(新機能・非互換なし)で、BG 手順・CPG 設計(同 aurora-postgresql16 ファミリ)・アプリ互換に差は出ない。16.14 は 16.13 の修正を全て含み、サポート/自動アップグレード猶予が長い。BG は Green で検証してから切替えるため「最新採用」のリグレッションリスクは緩和される。
- dr-work-record は 16.13 だが、mental-appointment 実施時点で 16.14 が available のため最新を採る(16.13 に揃える積極的理由=16.14 の既知 Aurora リグレッション等は現時点で無し)。
- ⚠️ 本番実施の直前に
describe-db-engine-versionsで再確認(16.15 等が Aurora に来ていれば候補に加える)。
9-B. federation gateway 特定 + ③ 用 read query
- 上流 gateway =
chronic-api(mental-online-karte・Hive Gateway /@theguild/federation-composition・chronic-api/gateway.config.ts/compose-supergraph.ts)。各 subgraph を supergraph に合成。認証は/auth発行の CAT(x-access-token)を subgraph へ伝播。 - subgraph = mental-appointment-service:
/graphql(YogaFederationDriver)。 - ③ 用 read query(app→DB フルパス):業務 Query は全て
@authenticated+@requiresScopes(未認証の DB 読取 query は無い)。用途別に2段構え:
| 目的 | クエリ | 認証 | DB 到達 |
|---|---|---|---|
| GraphQL 層 疎通(②相当) | { _service { sdl } } | 不要 | ✗(SDL 返すのみ) |
| app→DB フルパス(③) | { doctors { id } }(doctors 表・622行) | 要(x-access-token=ServiceAccount CAT・scope read:Doctor) | ✓ |
# ③ フルパス(要 ServiceAccount CAT): 踏み台→ALB→subgraph→DB
curl -s -XPOST "http://<ALB>/graphql" -H 'content-type: application/json' \
-H "x-access-token: <ServiceAccount CAT>" \
-d '{"query":"{ doctors { id } }"}' -w ' [%{http_code}]'
# 未認証で層だけ確認(DBは触らない)
curl -s -XPOST "http://<ALB>/graphql" -H 'content-type: application/json' \
-d '{"query":"{ _service { sdl } }"}' -w ' [%{http_code}]'CAT が用意できない場合、③は gateway
chronic-api経由の実 UI 操作(患者/管理画面)+ DB 直 psql(§付録)+ Datadog で代替。他の read query 候補:appointment(id)/appointments(ids)/appointmentAvailabilities(startDate,visitType)。
9-C. 東京(ap-northeast-1) クロスリージョン consumer 移行後再確認手順
BG Switchover では cluster endpoint / reader endpoint の DNS 名は保持(Green が同名を引き継ぐ)=接続文字列変更不要。切替時に既存接続は切断→再接続される。
password_encryption=md5も CPG 維持で認証断なし。以下を Switchover 後に確認:
| # | consumer | 再確認内容 |
|---|---|---|
| 1 | mental-appointment-service(本体・us-east-1 / ap-ne-1 ECS) | 両リージョン ECS healthy / /health 200 / { doctors }(③)/ Datadog エラー無 |
| 2 | online-ops-service(ap-ne-1) | ECS healthy / ops API で予約データ read 成功 / reader cluster-ro- 依存の機能疎通 / Datadog |
| 3 | Retool(SaaS) | mental-appointment を参照する Retool アプリでデータ表示成功(JDBC 再接続) |
| 4 | Trocco(SaaS・reader) | 次回スケジュール or テスト実行が成功(cluster-ro- 抽出)。転送先データ鮮度確認 |
- 全 consumer が プライベート経路(同一VPC / クロスリージョン VPC peering・fd-platform-vpc
10.10.0.0/16)。NAT 非経由なので NAT 側の設定変更は不要。 - reader endpoint 依存(②online-ops・④Trocco)があるため、Green の Reader も昇格後に稼働していることを確認。
9-D. staging UI から E2E 予約する手順(画面・ボタン・URL)
⚠️ 本番では実行しない。staging で PG16 移行前後の予約 CRUD を比較する。出典: Notion「時間帯予約QA進め方」「QA|メンタル最短予約」。
staging URL
| 画面 | URL |
|---|---|
| 患者WEB(予約・時間帯予約v2) | https://mental-reserve-patient.fstdr.app/v2/appointment/select-doctor-and-date |
| 管理画面(医師スケジュール登録=空き枠作成) | https://mental-reserve-admin.fstdr.app/admin/examination-schedule-management |
| 管理画面(予約一覧・事務局キャンセル) | https://mental-reserve-admin.fstdr.app/admin/appointments |
| 事務局/カルテ(ops・CLIUS/カルテ確認) | https://online-karte.fstdr.app/ops/mental |
本番は
https://mental-reserve-patient.fstdr.jp/appointment/...(テスト医師表示に?_alk=allowlist)。
前提(空き枠が無いと予約できない):管理画面(examination-schedule-management)で QA テスト医師のシフトを登録して空き枠を作る。テスト医師アカウント(staging・慢性期予約専用)=Notion「時間帯予約QA進め方」の一覧(qatest_manseiki_31〜36@fastdoctor.jp / password1! / 医師ID 1190,1191,1198〜1201)。医師枠が全て未配になると患者画面に空き枠(△/◯)が出る。
患者WEB 予約フロー(ボタン実体)
/v2/appointment/select-doctor-and-dateを開く- 訪問区分ドロップダウン 「選択してください」 → 「メンタルオンライン診療-初診」 選択 → 「決定」 ボタン
- カレンダーの 空き日時セル(△/◯)をタップ(
data-slot-status="t"=予約可) - 「患者情報入力へ」 ボタン
- (未認証時)ログイン:電話番号入力 → 生年月日選択 → 送信 → SMS PIN 入力で認証
- 患者選択(select-patient)→ 患者を選択
- 患者情報入力 → 次へ / 住所 → 次へ / 薬局情報 → 次へ
- 確認画面:「利用規約とプライバシーポリシーに同意する」 チェック → 「同意して申し込む」 ボタン
- 完了:「予約が完了しました / 仮予約が完了しました」 表示
PG16 移行 E2E(staging・Switchover 前後で同一手順)
- 予約作成(上記)→ DB 反映確認(
appointments/appointment_historiesに行追加)→ 予約詳細/編集/キャンセル(患者マイページ・admin 事務局キャンセル)→ ops でカルテ作成・CLIUS 送信確認。 - 移行前後で「予約作成成功・件数増加・カルテ/CLIUS 正常」を比較。差異が無ければ app→DB 経路健全。
参考(プログラム的手段):GraphQL createAppointment(input: CreateAppointmentInput!)(scope crate:Appointment・要 CAT)。appointmentAvailabilities(startDate,visitType) で空き取得 → createAppointment(businessDate,startAt,doctorId,visitType,patient/appointmentPatientId)。既存 E2E 実装: chronic-phase-appointment-service の test/post-appointments.e2e-spec.ts(Playwright ページオブジェクト: SelectDoctorAndDatePage/ConfirmPage 等)。
- ⚠️
saku:create-test-receptionskill は オンライン診療(急性期)receptions 用(backend-stg.fstdr.jp)で 別サービス→流用不可。 - ⚠️ Feature Flag(
ARCH-43-enable-query-appointment-v2-*)の状態で v2 予約の挙動が変わる。QA 前に FF 状態を確認。 - 後片付け:患者マイページ or admin 事務局キャンセル。
付録:踏み台経由 read-only 接続(コピペ可)
⚠️ Secret
mental-appointmentは値に制御文字を含むため、Python の strict JSON パースが失敗する(JSONDecodeError: Invalid control character)。strict=False必須(dr-work-record の Secret とは形式が異なる)。
export AWS_PROFILE=production-admin
R=us-east-1; BASTION=i-09384db1d690bcfd0; SECRET=mental-appointment; CL=mental-appointment; LPORT=15468
# 1) 踏み台→DB ポートフォワード
nohup aws ssm start-session --region $R --target $BASTION \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"$(aws rds describe-db-clusters --region $R --db-cluster-identifier $CL --query 'DBClusters[0].Endpoint' --output text)\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"$LPORT\"]}" > /tmp/pgt-$LPORT.log 2>&1 &
for i in $(seq 1 20); do grep -q "Waiting for connections" /tmp/pgt-$LPORT.log && break; sleep 1; done
# 2) Secret パース(★strict=False)
SEC=$(aws secretsmanager get-secret-value --region $R --secret-id $SECRET --query SecretString --output text)
export PGUSER=$(echo "$SEC" | python3 -c 'import sys,json;print(json.loads(sys.stdin.read(),strict=False)["DB_USERNAME"])')
export PGPASSWORD=$(echo "$SEC" | python3 -c 'import sys,json;print(json.loads(sys.stdin.read(),strict=False)["DB_PASSWORD"])')
export PGDATABASE=$(echo "$SEC" | python3 -c 'import sys,json,urllib.parse as u;print(u.urlparse(json.loads(sys.stdin.read(),strict=False)["DATABASE_URL"]).path.lstrip("/"))')
export PGHOST=127.0.0.1 PGPORT=$LPORT
# 3) preflight(read-only)
psql -tAF'|' -c "SELECT current_setting('server_version'), current_setting('rds.logical_replication'), current_setting('password_encryption'), current_setting('shared_preload_libraries');"
# 4) 全テーブル件数(exact・書込なし)
psql -tAF' ' -c "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"
# 5) 後片付け
pkill -f "localPortNumber.*$LPORT"