Initial Guide — Azure PostgreSQL: จาก server เปล่า ถึง state ปัจจุบัน

จุดประสงค์: หลัง provision Azure Database for PostgreSQL Flexible Server ขึ้นมาแล้ว ต้อง config อะไรต่อ เพื่อให้ได้ database + schema + master/config data เท่ากับ state ปัจจุบันของ DEV ตรวจสอบกับของจริงเมื่อ: 2026-09-21 — server sua-az-dev-sql-prostgres (RG sua-azure-nonprd, subscription 501c116b-493a-452e-a823-c7c0f443d01a, region southeastasia) คู่กับ: initial-guide-aks.md — เล่มนั้นคือ cluster เล่มนี้คือ data layer

การอ่าน tag ท้าย bullet:

⚠️ ข้อจำกัดของข้อมูล [repo]: local checkout ตามหลัง origin/development (Backend_UserService ตามหลัง 275 commit) — คำสั่ง git grep ในเอกสารนี้ยิงที่ origin/development แล้ว (ยกเว้น Backend_TaskService ที่ default branch คือ origin/master) แต่ก่อนใช้งานจริงให้ git fetch origin ก่อนเสมอ

service ที่ไม่มี source ในเครื่องเลย → ตรวจ [repo] ไม่ได้ ทุกแถวเป็น [live] จาก DB + [gap]: FX suite 6 ตัว (fx, fxcodex, fxcontract, fxfile, fxorchestrator, fxrate), log-service (มีโฟลเดอร์ Backend_LogService แต่ไม่มี Migrations) และ filter-service (ไม่มีโฟลเดอร์เลย) — รวม 8 service


0. ขอบเขต


0.1 ลำดับงานทั้งหมด (checklist — รายละเอียดอยู่ในหัวข้อที่อ้าง)

  1. Network ก่อนเสมอ — สร้าง Private Endpoint ใน subnet ที่ AKS route ถึง + ผูก Private DNS Zone Group privatelink.postgres.database.azure.com แล้ว link เข้า VNet ของ AKS → §2
  2. พิสูจน์ DNS จาก pod จริง ก่อนทำอย่างอื่น (kubectl run dnstest ... nslookup) — ผิดตรงนี้แล้วอาการจะไปโผล่ตอน deploy → §2
  3. สร้าง role sua-<env>-admin (+ app / etl ถ้าจะลอก DEV) → §3
  4. สร้าง database เปล่า ตามชื่อ/owner ในตาราง §4 — env ใหม่ใช้ prefix ให้ครบทุกตัว อย่าลอกที่ผสมกันของ DEV → §4
  5. ใส่ secret ลง Key Vault + สร้าง secret-provider ตาม objectName/objectAlias ของ service นั้น (key ไม่สม่ำเสมอ) → §4
  6. รัน EF migration ต่อ service — sentinel-gateway กับ workflow รันเองตอน pod start ที่เหลือ รันด้วยมือ → §5
  7. สร้าง partition LogDb.RequestLogs_YYYY_MM ล่วงหน้า ไม่งั้น insert log ไม่ได้ → §6 ถังพิเศษ
  8. ค่อย deploy/start pod — seeder ถัง B เป็น IHostedService ต้องมีตารางอยู่ก่อน → §6 ถัง B
  9. copy master data ถัง C จาก DEV แบบเลือกตาราง (pg_dump --data-only -t ...) อย่าใช้ clone-dev-to-sit.sh ตรง ๆ → §6 ถัง C + §7
  10. generate ApiClients / ApiClientScopes / ApiKeys ใหม่ ห้าม copy ข้าม env แล้วเอาค่าใหม่ไปใส่ Key Vault → §6
  11. verify ด้วย query ใน §8 เทียบกับ EF migration และจำนวนแถวใน §6
  12. เคลียร์ [gap] ใน §9 ก่อนถือว่าเสร็จ

1. ค่า server ปัจจุบัน (ใช้เป็น target ตอน provision)

หัวข้อ ค่าจริง tag
Engine PostgreSQL 17 (runtime 17.11, param server_version = 17.8) [live]
SKU Standard_B2ms / tier Burstable [live]
Storage 32 GB Premium_LRS tier P4, IOPS 120, autoGrow = Disabled [live]
HA Disabled (ไม่มี standby) [live]
Backup retention 7 วัน, geoRedundant Disabled [live]
Auth passwordAuth: Enabled, activeDirectoryAuth: Disabled (ไม่มี Entra ID auth) [live]
Network publicNetworkAccess: Disabled, delegatedSubnetResourceId: null → Private Endpoint ไม่ใช่ VNet injection [live]
Encoding ทุก DB UTF8 / collate en_US.utf8 [live]
require_secure_transport on → connection string ต้องมี SSL Mode=Require [live]
max_connections 859 [live]

เรื่อง parameter — จุดที่คนมักเสียเวลา [live]

# ตรวจ parameter ที่คนตั้งเอง (ตัด platform ออกด้วยตา)
az postgres flexible-server parameter list -g <RG> -s <SERVER> \
  --query "[?source=='user-override'].{n:name,v:value,d:defaultValue}" -o table

2. Network + DNS (ทำก่อนอย่างอื่น ไม่งั้นต่อไม่ติดแล้วไล่ผิดจุด)

# เช็กจากใน cluster ว่า pod resolve ได้จริงไหม
kubectl -n superappdev run dnstest --rm -it --restart=Never --image=busybox:1.36 -- \
  nslookup sua-az-dev-sql-prostgres.postgres.database.azure.com

สิ่งที่ต้องทำตอน provision server ใหม่:


3. Role / สิทธิ์ (ทำก่อนสร้าง DB)

Role ที่มีจริงบน server [live]

role super createdb createrole login replication หมายเหตุ
SysEximAdmin ✗ ✓ ✓ ✓ ✗ server admin ของ Flexible Server (member ของ azure_pg_admin)
sua-dev-admin ✗ ✓ ✗ ✓ ✓ member ของ azure_pg_admin — role ที่ service ใช้จริง
sua-dev-app ✗ ✗ ✗ ✓ ✗ มีอยู่แต่ไม่มี service ไหนใช้
sua-dev-etl ✗ ✗ ✗ ✓ ✗ มีอยู่แต่ไม่พบการใช้
replication ✗ ✗ ✗ ✓ ✓ ของ platform
-- ทำตาม DEV แบบตรงไปตรงมา
CREATE ROLE "sua-<env>-admin" WITH LOGIN CREATEDB REPLICATION PASSWORD '<...>';
GRANT azure_pg_admin TO "sua-<env>-admin";
CREATE ROLE "sua-<env>-app" WITH LOGIN PASSWORD '<...>';
CREATE ROLE "sua-<env>-etl" WITH LOGIN PASSWORD '<...>';

4. สร้าง Database (ชื่อ + owner + secret ที่ผูกกัน)

service DEV database KV secret (sua-azure-nonprd-kv) schema ที่มี EF migration (applied / last)
user (auth) AuthDb (เปล่า) dev--user-service--UserService--Database--Primary--ConnectionString ⚠️ คนละรูปแบบกับตัวอื่น public, Config 81 / 20260918112151_SeedCreditLimitForwardContract
centralized DEV_CentralizedDb dev--centralized-service--ConnectionStrings--Default public, Config, los, ApplicationTerms 11 / 20260904143632_AddMatchedAmloTypesToScreenedCompany
codex DEV_CodexDb dev--codex-service--ConnectionStrings--Default public 10 / 20260609090304_AddAppRegistrationUrlFields
orchestrator DEV_OrchestratorDb dev--orchestrator-service--ConnectionStrings--Default public, Orchestration 4 / 20260702102652_AddActivityLogPayloads
sentinel-gateway (bff) DEV_SentinelGatewayDB dev--sentinel-gateway--ConnectionStrings--SentinelGatewayDB public 7 / 20260409065856_RemoveOnboardingSessions
task DEV_TaskDb dev--task-service--ConnectionStrings--Default public 9 / 20260808160715_AddXminConcurrencyToTaskTransaction
workflow DEV_WorkflowDb dev--workflow-service--ConnectionStrings--Default public 19 / 20260702111222_AddPublisherAndServiceCallLogs
notification NotificationDb (เปล่า) dev--notification-service--ConnectionStrings--Default public 11 / 20260805045911_AddAppCodeToInAppNotifications
thirdparty ThirdPartyDb (เปล่า) dev--thirdparty-service--ConnectionStrings--Default public, Config 9 / 20260617084844_FixSegmentMappingValueGeneration
filemanagement FileManagementDB (เปล่า) dev--filemanagement-service--ConnectionStrings--Default public 10 / 20260409065856_RemoveOnboardingSessions
filter FilterDb (เปล่า) dev--filter-service--ConnectionStrings--Default public 16 / 20260604100212_moveDisPlayStatus
log LogDb (เปล่า) dev--log-service--ConnectionStrings--Default public, Config 3 / 20260903070030_AddRequestLogs
fxcontract DEV_FXContractDb dev--fxcontract-service--ConnectionStrings--Default public, Config 59 / 20260916102939_AddCutLimitStatusToForwardContractTransactionTable
fx (thirdpartyfx) DEV_FXDb dev--fx-service--ConnectionStrings--DefaultConnection public 4 / 20260609090001_RestoreSegmentMappingsAuditColumns
fxcodex DEV_FxCodexDb dev--fxcodex-service--ConnectionStrings--Default public, Config 3 / 20260623072254_CreateConfigurationMasterTable
fxorchestrator DEV_FxOrchestratorDb dev--fxorchestrator-service--ConnectionStrings--Default public, Orchestration 1 / 20260612040112_InitialCreate
fxrate DEV_FxRateDb dev--fxrate-service--ConnectionStrings--Default public 19 / 20260814042013_AddCounterRateIsActive
fxfile DEV_FXFileDb dev--fxfile-service--ConnectionStrings--Default public, Config, File 11 / 20260813022637_ChangeSizeFileToMegabytes

ที่มาของแต่ละคอลัมน์ — อย่าเหมารวม:

รูปแบบ connection string ที่ใช้จริง (จาก dev--task-service--ConnectionStrings--Default, ค่า password ตัดออก) [live]

Host=<server>.postgres.database.azure.com;Port=5432;Database=<Db>;Username=sua-dev-admin;Password=<secret>;
Pooling=true;Maximum Pool Size=10;Minimum Pool Size=0;Connection Idle Lifetime=300;
Timeout=15;CommandTimeout=30;SSL Mode=Require;Trust Server Certificate=true

5. ลง schema (EF Core migration) — ใครรันให้ ใครต้องรันเอง

🔴 จุดสำคัญที่สุดของเอกสารนี้: ระบบนี้ไม่มี automated migration pipeline

หลักฐานว่ามันหลุดจริง — AuthDb บน DEV ไม่ตรงกับ code [live] + [repo] (เทียบ __EFMigrationsHistory กับไฟล์ใน origin/development 2026-09-21)

ลำดับที่ต้องทำ (ห้ามสลับ):

  1. สร้าง DB เปล่า + role ตาม §3–§4
  2. รัน migration ต่อ service (sentinel-gateway, workflow จะรันเองตอน pod start — ที่เหลือรันเอง)
  3. ค่อย deploy/start pod — seeder ใน §6 bucket B เป็น IHostedService ที่ต้องการตารางอยู่ก่อนแล้ว
# ต่อ service (รันจาก repo ของ service นั้น)
export ConnectionStrings__Default="Host=...;Database=<Db>;Username=...;Password=...;SSL Mode=Require;Trust Server Certificate=true"
dotnet ef database update \
  -p src/<Svc>02.Infrastructure -s src/<Svc>01.API

# หรือทำเป็น idempotent SQL ให้ DBA รัน (ปลอดภัยกว่าสำหรับ UAT/PROD)
dotnet ef migrations script --idempotent \
  -p src/<Svc>02.Infrastructure -s src/<Svc>01.API -o migrate.sql

6. Master / Config data — ที่มาแบ่งเป็น 3 ถัง

นี่คือคำตอบของคำถาม “ทำยังไงให้ข้อมูล master/config ครบเท่าปัจจุบัน”

ถัง A — อยู่ใน migration (InsertData / migrationBuilder.Sql) → มาเองตอนรัน §5

service ไฟล์ migration ที่มี InsertData ข้อมูลที่ได้ [repo]
user (auth) 8 ไฟล์ — SeedRoleApiPermissionGrant, SeedApiPermissionDefinitionsForServices, SeedFrontendPermissionsAndAdminGrants, SeedAdditionalRoleApiPermissionGrants, SeedUserProfileReadPermission, SeedAdminTermsConditionsRoleGrants, SeedCreditLimitForwardContract ฯลฯ menu / permission / role / api-permission grant ✓
codex 1 — 20260527091846_AddDocumentNumberPolicy DocumentNumberPolicies (47 แถวบน DEV) ✓
sentinel-gateway 2 — 20260224072152_InitialCreate, 20260324000000_RemoveYarpClusterAndDefaultRoles ของเก่า (ตัวหลังลบ default role ทิ้ง) ✓
centralized / thirdparty / orchestrator / notification / workflow / filemanagement 0 — ✓
task 0 (ตรวจที่ origin/master) — ✓

⚠️ แก้ความเข้าใจผิดที่เจอตอนตรวจ: notification (3), workflow (1), filemanagement (1) มี migrationBuilder.Sql(...) จริง แต่เป็น DDL ล้วน (DROP TABLE IF EXISTS, CREATE UNIQUE INDEX, ALTER COLUMN ... TYPE) ไม่ใช่ข้อมูล — git grep "INSERT INTO" origin/development -- "*Migrations*" ในทั้ง 3 repo ได้ 0 บรรทัด [repo]

ตรวจเองได้ด้วย: git grep -l "InsertData" <branch> -- "*Migrations*" และ git grep -n "INSERT INTO" <branch> -- "*Migrations*"

ถัง B — seeder ตอน start pod (IHostedService) → มาเองตอน pod ขึ้น

service seeder ตารางปลายทาง พฤติกรรม [repo]
user ConfigDataSeeder Config.AppConfigurations (10 แถว) insert เฉพาะ Key ที่ยังไม่มี — ไม่ update ของเดิม ✓
user ErrorCodeDataSeeder Config.CustomErrorCodes (10 แถว) เหมือนกัน ✓
notification EmailTemplateDataSeeder email_template_keys 5 key + email_macros 8 macro เท่านั้น (isSystem: true) skip ที่มีอยู่แล้ว ✓

ถัง C — ไม่มีที่มาใน code → ต้อง copy จาก DEV หรือกรอกผ่าน Admin Portal

DB ตาราง (จำนวนแถวบน DEV 2026-09-21) หมายเหตุ
NotificationDb email_templates 19, email_template_keys อีก 19 จาก 24, email_macros อีก 19 จาก 27 ส่วนที่ seeder ไม่ได้ลง (ดูถัง B)
DEV_CodexDb MdmEntries 68, MdmEntryLocalizations 125, MdmCategories 4, MdmGroups 1, CmsContentConfig 12, AppRegistration 4 MDM = master data กลาง จัดการผ่าน Admin Portal (DocumentNumberPolicies ไม่อยู่ถังนี้ — มากับ migration ถัง A)
ThirdPartyDb CurrencyMaster 9, SpreadConfig 81, SwapPointConfig 54, SegmentConfig 3, SegmentMapping 1, FxConfiguration 2 config อัตรา/สเปรด
DEV_FXDb SpreadConfigs 81, SwapPointConfigs 54, CurrencyMasters 9, SegmentConfigs 3, FxConfigurations 2 ชุดเดียวกับ ThirdPartyDb (กำลังย้ายบ้าน) [assume]
DEV_FxRateDb SpreadConfig 108, ForwardPoint 54, SegmentMapping 27, CounterRate 21, SegmentConfig 5 บางส่วน sync จากระบบภายนอก [gap]
DEV_FxCodexDb ConfigurationMaster 53, CurrencyMaster 9
DEV_FXContractDb Holiday 2301, UnderlyingType 6, CreditLimitUnderlying 3, ForwardContractNumberControl 2 Holiday มาจาก sync job (HolidaySyncRun 10 รอบ) ไม่ใช่ seed — env ใหม่ต้องรัน sync [live]
DEV_CentralizedDb ApplicationTermsVersion 72 + ...Condition 72 + ...Content 72, ApplicationTermsDocument 8 + localization 8, DocumentNumberRunning 2 T&C จัดการผ่าน Admin Portal
DEV_TaskDb WorkType 1, WorkTypeRole 2 ยืนยันแล้วว่าไม่มี insert ใน migration (origin/master)
DEV_WorkflowDb WfTemplate 1, WfPhaseTemplate 2, WfStepTemplate 2, WfStepAssigneeTemplate 2 ยืนยันแล้วว่า migration raw-SQL ตัวเดียวเป็น ALTER COLUMN ไม่ใช่ข้อมูล
DEV_CentralizedDb schema los amlo_screened_company 9609, amlo_screening_match 66607, amlo_ingestion_batch 10 มาจาก ingestion job ไม่ใช่ seed
ทุก DB ที่มี schema Config ยกเว้น AuthDb AppConfiguration(s) 10, CustomErrorCode(s) 10 🔴 [gap] ตารางถูกสร้างโดย migration แต่ไม่พบ seeder ใน repo → แถวมาจากมือคนหรือโค้ดที่ไม่ได้อยู่ใน checkout

ถังพิเศษ — partition ที่ต้องมีล่วงหน้า

ข้อควรระวังที่จะทำให้ copy ผิด:


7. วิธี copy data จาก DEV (ถ้าเลือกทางนี้)

มี script ของเดิมเป็นแบบอย่าง: Backend_Iac/scripts/clone-dev-to-sit.sh [repo]

# เลือกตารางที่ต้องการจริง ๆ เท่านั้น
pg_dump -h $HOST -U $USER --data-only --no-owner --no-acl \
  -t 'public."MdmEntries"' -t 'public."MdmEntryLocalizations"' \
  -t 'public."MdmCategories"' -t 'public."MdmGroups"' \
  DEV_CodexDb | psql -h $HOST -U $USER -d <ENV>_CodexDb

8. Verify ว่าถึง state ปัจจุบันแล้ว

-- 1. schema version ต่อ DB (เทียบ MigrationId ไม่ใช่ ProductVersion)
SELECT count(*) AS applied, max("MigrationId") AS last FROM "__EFMigrationsHistory";

-- 2. schema ครบไหม
SELECT nspname FROM pg_namespace
WHERE nspname NOT LIKE 'pg\_%' AND nspname <> 'information_schema';

-- 3. master/config มีข้อมูลไหม (รันใน DB เป้าหมาย)
SELECT schemaname, relname, n_live_tup FROM pg_stat_user_tables
WHERE n_live_tup > 0 ORDER BY n_live_tup DESC;

เกณฑ์ผ่าน เทียบกับคอลัมน์ “EF migration (applied / last)” ใน §4 และจำนวนแถวใน §6 ถัง C

🔴 count / max("MigrationId") เป็นแค่ smoke check เชื่อไม่ได้ — เคส AuthDb บน DEV พิสูจน์แล้ว: max ตรงกับไฟล์ล่าสุดใน origin/development เป๊ะ (20260918112151_SeedCreditLimitForwardContract) แต่ยังค้าง 2 migration และมี orphan 1 ตัว (ดู §5) → การเทียบ max อย่างเดียวจะรายงานว่า "sync แล้ว" ทั้งที่ไม่ใช่

ของจริงต้อง set-diff:

# วิธีที่ง่ายสุด — จะพิมพ์ (Pending) ต่อท้ายตัวที่ยังไม่ได้รัน
dotnet ef migrations list \
  -p src/<Svc>02.Infrastructure -s src/<Svc>01.API \
  --connection "Host=...;Database=<Db>;Username=...;Password=...;SSL Mode=Require;Trust Server Certificate=true"
-- ฝั่ง DB: เอา list นี้ไป diff กับชื่อไฟล์ใน Migrations/ (ตัด Designer/Snapshot ออก)
SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY 1;

ดูทั้งสองทาง: ไฟล์ที่ยังไม่ applied = ต้องรันเพิ่ม, applied ที่ไม่มีไฟล์แล้ว = สายแตก ต้องตัดสินใจว่าจะ reconcile ยังไง

kubectl -n <ns> exec deploy/<svc> -- env | grep -i ConnectionStrings \
  | sed 's/Password=[^;]*/Password=***/g'

# ดู log seeder
kubectl -n <ns> logs deploy/<svc> | grep -i "Seeded\|already exist\|Failed to seed"

ผ่าน = เห็น Seeded N configuration entries หรือ All configuration entries already exist — ถ้าเห็น Failed to seed ... Run migrations first แปลว่าลำดับใน §5 ผิด


9. สรุปช่องโหว่ที่ต้องเคลียร์ก่อน provision env ใหม่

# เรื่อง ทำไมสำคัญ
1 [gap] ไม่มี automated migration — 16/18 service ต้องรัน dotnet ef ด้วยมือ ไม่รู้ว่าใครถือ runbook env ใหม่จะได้ schema ไม่ครบและไม่มีใครรู้ตัว
2 [gap] pod ใน AKS resolve FQDN ของ PG ด้วยอะไร (ไม่มี private DNS zone ใน subscription) ต่อ DB ไม่ติดตั้งแต่ pod แรก
3 [gap] แถว Config.AppConfiguration(s)/CustomErrorCode(s) ใน DB ที่ไม่ใช่ Auth ไม่มี seeder ใน repo config หาย ต้อง copy มือ
4 [gap] FX suite ไม่มี repo ในเครื่อง — ที่มา master data ของ 6 DB ตรวจไม่ได้ FX ขึ้น env ใหม่ไม่ได้จนกว่าจะได้ repo
5 service ใช้ sua-dev-admin (azure_pg_admin) ต่อ DB 1 credential รั่ว = เข้าถึงทุก DB รวม UAT
6 clone-dev-to-sit.sh พังกับ DB ที่มี schema นอก public และอ้าง DB ที่ไม่มีแล้ว copy แล้ว PK ชน
7 Burstable Standard_B2ms + storage 32 GB autoGrow: Disabled + HA off + backup 7 วัน เหมาะ DEV เท่านั้น — UAT/PROD ต้องยกระดับก่อน
8 SentinelGateway กลืน exception ตอน migrate fail pod เขียว แต่ schema ไม่ขึ้น
9 [gap] ไม่รู้ว่าใครสร้าง partition LogDb.RequestLogs_YYYY_MM (ปัจจุบันมีถึง 2027-08) env ใหม่ INSERT log ไม่ได้
10 AuthDb บน DEV กับ code แตกสายกัน (ค้าง 2 / มีของที่ code ไม่มีแล้ว 1) env ใหม่ที่รันจาก code จะได้ schema ไม่เท่า DEV
11 [gap] PgBouncer connection string ถูก mount ให้ 4 service แต่ไม่มี PgBouncer ทั้งบน server และใน cluster ลอกไป env ใหม่แล้วต่อไม่ติด หรือ config ตาย
12 [gap] consent, fx-report, companies-migration มี secret แต่หา DB คู่ไม่เจอ ลืม provision DB ของ service เหล่านี้