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(RGsua-azure-nonprd, subscription501c116b-493a-452e-a823-c7c0f443d01a, region southeastasia) คู่กับ: initial-guide-aks.md — เล่มนั้นคือ cluster เล่มนี้คือ data layer
การอ่าน tag ท้าย bullet:
[live]— query จาก Azure / จาก PG จริงวันที่ 2026-09-21[repo]— อ่านจาก source ในBackend_*/Backend_Iac[gap]— หาไม่เจอว่าใครทำ/ทำยังไง ต้องถามเจ้าของระบบก่อนใช้[assume]— เดาจาก convention ยังไม่ verify
⚠️ ข้อจำกัดของข้อมูล
[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. ขอบเขต
- ครอบคลุม: server provision เสร็จ → network/DNS → role → สร้าง DB → schema (EF migration) → master/config data → verify
- ไม่ครอบคลุม: transactional data (ใบคำขอ, task, notification log), การ provision server เอง (SKU/storage ดู §1 เป็น target)
- ไม่นับเป็น DB ของระบบ (อยู่บน server เดียวกันแต่เป็นของเครื่องมือ/ขยะ)
[live]UAT_SonarDb(431 MB),UAT_DependencyTrackDb(2717 MB) — เครื่องมือ DevSecOps ดู [dependency-track-owasp-setup]coverage_*8 ตัว — ของเหลือจาก test run ลบทิ้งได้azure_maintenance,azure_sys,postgres— ของ platform
0.1 ลำดับงานทั้งหมด (checklist — รายละเอียดอยู่ในหัวข้อที่อ้าง)
- Network ก่อนเสมอ — สร้าง Private Endpoint ใน subnet ที่ AKS route ถึง + ผูก Private DNS Zone Group
privatelink.postgres.database.azure.comแล้ว link เข้า VNet ของ AKS → §2 - พิสูจน์ DNS จาก pod จริง ก่อนทำอย่างอื่น (
kubectl run dnstest ... nslookup) — ผิดตรงนี้แล้วอาการจะไปโผล่ตอน deploy → §2 - สร้าง role
sua-<env>-admin(+app/etlถ้าจะลอก DEV) → §3 - สร้าง database เปล่า ตามชื่อ/owner ในตาราง §4 — env ใหม่ใช้ prefix ให้ครบทุกตัว อย่าลอกที่ผสมกันของ DEV → §4
- ใส่ secret ลง Key Vault + สร้าง secret-provider ตาม
objectName/objectAliasของ service นั้น (key ไม่สม่ำเสมอ) → §4 - รัน EF migration ต่อ service —
sentinel-gatewayกับworkflowรันเองตอน pod start ที่เหลือ รันด้วยมือ → §5 - สร้าง partition
LogDb.RequestLogs_YYYY_MMล่วงหน้า ไม่งั้น insert log ไม่ได้ → §6 ถังพิเศษ - ค่อย deploy/start pod — seeder ถัง B เป็น
IHostedServiceต้องมีตารางอยู่ก่อน → §6 ถัง B - copy master data ถัง C จาก DEV แบบเลือกตาราง (
pg_dump --data-only -t ...) อย่าใช้clone-dev-to-sit.shตรง ๆ → §6 ถัง C + §7 - generate
ApiClients/ApiClientScopes/ApiKeysใหม่ ห้าม copy ข้าม env แล้วเอาค่าใหม่ไปใส่ Key Vault → §6 - verify ด้วย query ใน §8 เทียบกับ EF migration และจำนวนแถวใน §6
- เคลียร์
[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]
- ทั้ง server มี parameter
source = user-override40 ตัว แต่เช็กแล้วเป็นของ Azure platform ทั้งหมด (archive_command,data_directory,ssl_*,shared_memory_size, ...) — ไม่มี tuning ที่ทีมตั้งเอง (ไม่มีwork_mem,max_connections,statement_timeoutที่ override) - → ตอน provision server ใหม่ ไม่ต้องไปไล่ปรับ parameter ใด ๆ ปล่อย default ของ Azure ได้
shared_preload_libraries = pg_cron,pg_stat_statementsเป็นค่าที่ Azure ใส่มา แต่pg_cronไม่ได้ถูกเปิดใช้จริง —cron.jobไม่มีอยู่ (relation "cron.job" does not exist) → ไม่มี scheduled job ใน DB ที่ต้อง re-createazure.extensions= ว่าง (system-default) → ไม่มี extension ที่ allowlist ไว้- ทุก DB ของระบบมี extension แค่
plpgsql 1.0ตัวเดียว → ไม่ต้องCREATE EXTENSIONอะไรเลย (ไม่มี uuid-ossp, pgcrypto, postgis)
# ตรวจ 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 (ทำก่อนอย่างอื่น ไม่งั้นต่อไม่ติดแล้วไล่ผิดจุด)
- Public access ปิด → เข้าได้ทาง Private Endpoint เท่านั้น
[live] - Private Endpoint:
sua-azure-nonprd-pep-psql[live]- subnet
sua-az-dev-db-snet(ในsua-azure-nonprd-vnet) - private IP =
172.29.83.4
- subnet
- ⚠️ PE ตัวนี้ไม่มี
privateDnsZoneConfigsและทั้ง subscription ไม่มี zoneprivatelink.postgres.database.azure.com[live] - VNet
sua-azure-nonprd-vnetตั้งdhcpOptions.dnsServers = null→ ใช้ Azure-provided DNS[live] - ที่เครื่อง dev ต่อติดเพราะ DNS ของ corp (
stmwifidhcpap1.exim.go.th/ 10.0.6.69) ตอบsua-az-dev-sql-prostgres.privatelink.postgres.database.azure.com → 172.29.83.4[live] - 🔴
[gap]ยังไม่ได้พิสูจน์ว่า pod ใน AKS resolve ชื่อนี้ด้วยอะไร (ไม่มี private DNS zone ใน subscription นี้ + VNet ใช้ Azure DNS) เป็นไปได้ว่า zone อยู่ hub subscription อื่น หรือมี conditional forwarder ที่ on-prem DNS — ต้องเช็กก่อน provision env ใหม่ ไม่งั้น pod จะName or service not known
# เช็กจากใน 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 ใหม่:
- สร้าง PE ใน subnet ที่ AKS route ถึง + ผูก Private DNS Zone Group (
privatelink.postgres.database.azure.com) link เข้า VNet ของ AKS — อย่าเลียนแบบของ DEV ที่ไม่มี zone group เพราะมันพึ่ง corp DNS อยู่[assume] - firewall rule ใช้ไม่ได้:
az postgres flexible-server firewall-rule listตอบERROR: Firewall rule operations cannot be requested for a server that doesn't have public access enabled[live]
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 |
- membership
SysEximAdmin → sua-dev-admin / sua-dev-app / sua-dev-etlเป็นผลข้างเคียงของ PG 16+ (ผู้สร้าง role ได้ membership อัตโนมัติ) ไม่ใช่การออกแบบสิทธิ์[live] - 🔴 ข้อสังเกตด้าน security: connection string ของทุก service ใช้
Username=sua-dev-adminซึ่งเป็น member ของazure_pg_admin(อ่าน/เขียนได้ทุก DB บน server รวม UAT) — ยืนยันจาก secretdev--task-service--ConnectionStrings--Default[live]- env ใหม่ ควรสร้าง least-privilege role ต่อ service แล้วเลิกใช้ admin แต่ ถ้าจะ clone DEV 1:1 ต้องใช้ admin ไม่งั้น migration จะ fail ตอน
CREATE SCHEMA[assume]
- env ใหม่ ควรสร้าง least-privilege role ต่อ service แล้วเลิกใช้ admin แต่ ถ้าจะ clone DEV 1:1 ต้องใช้ admin ไม่งั้น migration จะ fail ตอน
- schema
publicทุก DB เป็น ACL default (azure_pg_admin=UC/azure_pg_admin,=U/azure_pg_admin) → ไม่มีการ GRANT เพิ่มที่ต้องทำซ้ำ[live]
-- ทำตาม 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 ที่ผูกกัน)
- naming ของ server นี้ไม่สม่ำเสมอ: DEV บางตัว prefix
DEV_บางตัวเปล่า (ของเก่า), UAT ใช้UAT_ครบ ดู [pg-server-db-naming-scheme][live] - env ใหม่ควรใช้ prefix ให้ครบทุกตัว อย่าลอก DEV ที่ผสมกัน
[assume] - owner ปัจจุบันสลับกันระหว่าง
SysEximAdminกับsua-dev-adminแบบไม่มี pattern → env ใหม่ตั้งให้เป็นตัวเดียวกันทั้งหมด[live]
| 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 |
ที่มาของแต่ละคอลัมน์ — อย่าเหมารวม:
- EF migration =
[live]query__EFMigrationsHistory2026-09-21 - KV secret =
[repo]จากgrep -h "objectName: dev--.*Connection" Backend_Iac/src/yamls/dev/superapp/secret-providers/*.yaml(ครบทั้ง 21 ไฟล์ อ่านได้โดยไม่ต้องเปิดค่า secret) - DEV database =
[assume]จาก convention + [pg-server-db-naming-scheme] — ยืนยันจริงแค่DEV_TaskDb(อ่านจาก secretdev--task-service--ConnectionStrings--Default) ตัวอื่นต้องเปิด connection string ของ service นั้นดูก่อนเชื่อ ดู [uat-taskservice-dev-serviceprefix-leak] ที่เคยพลาดเพราะเชื่อ prefix - 🔴
[gap]มี secret-provider แต่ไม่มีแถวในตารางนี้ เพราะหา DB คู่ไม่เจอ:consent-secret-provider.yaml,fx-report-secret-provider.yaml, และ secretdev--companies-migration-service--ConnectionStrings--PgConnection→ ต้องเช็กว่าใช้ DB ไหน (หรือเลิกใช้แล้ว) ก่อน provision env ใหม่ fx-serviceกับthirdpartyfx-serviceเป็นคนละ secret-provider แต่ทั้งคู่ชี้DEV_FXDbในตารางนี้ →[assume]ต้องยืนยัน
รูปแบบ 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
- ⚠️ key ใน app config ไม่สม่ำเสมอ: ส่วนใหญ่เป็น
ConnectionStrings__Defaultแต่user-serviceใช้UserService__Database__Primary__ConnectionString,sentinel-gatewayใช้ConnectionStrings__SentinelGatewayDB,fx-service/thirdpartyfx-serviceใช้...--DefaultConnection→ ตอนสร้าง env ใหม่ต้องลอกobjectName/objectAliasจาก secret-provider ของ service นั้นตรง ๆ[repo] - ค่า pooling ชุดนี้มาจาก
Backend_Iac/scripts/flip-pg-pooling-dev.ps1(2026-07-05) — env ใหม่ตั้งตั้งแต่แรกได้เลย ไม่ต้องรัน script[repo] - 🔴
[gap]PgBouncer คลุมเครือ — secret-provider ของcentralized,fxcodex,fxcontract,fxorchestratormount...--ConnectionStrings--PgBouncerเข้ามาด้วย[repo]แต่ server ไม่มี parameterpgbouncer.*เลย (tier Burstable ไม่รองรับ built-in PgBouncer)[live]และไม่พบ manifest PgBouncer ที่ deploy เองในBackend_Iac/src/yamls→ ต้องเปิดค่า secret ดูว่าชี้ไปไหน (port 6432? host อื่น?) ก่อนลอกไป env ใหม่
5. ลง schema (EF Core migration) — ใครรันให้ ใครต้องรันเอง
🔴 จุดสำคัญที่สุดของเอกสารนี้: ระบบนี้ไม่มี automated migration pipeline
- ค้น
Program.csของทุก service (ทั้ง local และorigin/development) เจอDatabase.Migrate()แค่ 2 ตัว[repo]Backend_SentinelGatewayService/src/SentinelGateway01.API/Program.cs:149→DatabaseMigrationRunner.TryMigrateAsync(...)— ห่อ try/catch กลืน exception (migrate fail = pod ยัง start ปกติ แล้วไปพังตอน query)Backend_WorkflowService/src/WorkFlow01.API/Program.cs:91→dbContext.Database.Migrate()
- อีก 9 service ที่มี source ในเครื่อง ยืนยันแล้วว่าไม่มี migration runner ใน
Program.csเลย[repo]—user,centralized,codex,consent(ไม่มี DB ใน §4),filemanagement,notification,orchestrator,task,thirdparty - อีก 8 ตัวตรวจไม่ได้ เพราะไม่มี source ในเครื่อง
[gap]—fx6 ตัว +log+filter - (2 auto + 9 ยืนยันว่าไม่มี + 8 ตรวจไม่ได้ = 19 service ที่แตะ PG)
- ค้น
dotnet ef/efbundleในpipelines/*.yml,Dockerfile,Backend_Iacของ repo ที่มีในเครื่อง ไม่เจอเลย[repo] - 🔴
[gap]→ แปลว่า schema ปัจจุบันถูกลงด้วย คนรันdotnet ef database updateชี้ไป env นั้นด้วยมือ ต้องยืนยันกับทีมว่าใครถือ runbook นี้ ก่อน provision env ใหม่
หลักฐานว่ามันหลุดจริง — AuthDb บน DEV ไม่ตรงกับ code [live] + [repo] (เทียบ __EFMigrationsHistory กับไฟล์ใน origin/development 2026-09-21)
- applied 81 / ไฟล์ใน repo 82
- ยังไม่ได้รัน 2 ตัว:
20260609094448_ReconcileAppIdFromCodex,20260917091507_ApiKeyVersionConcurrencyToken - รันไปแล้วแต่ไฟล์ไม่มีใน repo แล้ว 1 ตัว:
20260420100951_UpdateCompanyAddress→ ไม่ใช่แค่ "ตามหลัง" แต่ สาย migration แตกกัน env ใหม่ที่รันจาก code จะได้ schema ไม่เท่ากับ DEV เป๊ะ - (migration ตัวล่าสุด
20260918112151_SeedCreditLimitForwardContractลงแล้วทั้งคู่)
ลำดับที่ต้องทำ (ห้ามสลับ):
- สร้าง DB เปล่า + role ตาม §3–§4
- รัน migration ต่อ service (
sentinel-gateway,workflowจะรันเองตอน pod start — ที่เหลือรันเอง) - ค่อย 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 ที่มีอยู่แล้ว | ✓ |
🔴 จุดที่หลอกตา: DEV มี
email_template_keys24,email_macros27,email_templates19 แต่ seeder ลงให้แค่ 5 + 8 → ส่วนที่เหลือทั้งหมด รวมemail_templatesทุกแถว ไม่ได้มาจาก code ต้องอยู่ในถัง C[repo]+[live]⚠️ seeder จับ exception แล้ว log
"Failed to seed configuration data — tables may not exist yet. Run migrations first."→ ถ้ารันก่อน migration มันจะเงียบ ๆ ไม่ seed แล้ว service ขึ้นมาแบบไม่มี config[repo]⚠️ เพราะ seeder ข้ามคีย์ที่มีอยู่แล้ว → แก้ค่า default ใน code แล้วจะไม่มีผลกับ DB ที่ seed ไปแล้ว ต้อง UPDATE เอง
ค่า
Config.AppConfigurationsถูกอ่านผ่าน Postgres LISTEN/NOTIFY → Redis (ConfigurationLoaderService) → Redis ต้องพร้อมด้วย ไม่งั้น config ไม่กระจาย[repo]
ถัง 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 ที่ต้องมีล่วงหน้า
LogDb.RequestLogsเป็น partitioned table (relkind = 'p') มี partition รายเดือนสร้างไว้ล่วงหน้าถึงRequestLogs_2027_08(RequestLogs_2026_09…RequestLogs_2027_08)[live]- 🔴
[gap]ไม่มี source ของ log-service ในเครื่อง (ไม่มีโฟลเดอร์Migrations) → ไม่รู้ว่าใครสร้าง partition ชุดนี้ (migration / คน / job) — env ใหม่ถ้าไม่มี partition INSERT จะ errorno partition of relation found for row
ข้อควรระวังที่จะทำให้ copy ผิด:
- ชื่อตารางไม่สม่ำเสมอ:
Config.AppConfigurations(พหูพจน์) ใน Auth/Centralized/ThirdParty/Log แต่ FX ใช้Config.AppConfiguration(เอกพจน์);FileManagementDBเก็บAppConfigurations/CustomErrorCodesไว้ในpublicไม่ใช่Config;NotificationDbทิ้ง schemaConfigไปแล้ว (migration20260706065724_DropConfigSchemaTables)[live] - ใน
AuthDbส่วนที่มาจาก migration (ถัง A) vs ที่ห้าม copy ปนกันอยู่:- copy/seed ได้:
Menus38,Permissions45,MenuPermissions49,Roles18,RolePermissions83,ApiPermissionDefinitions149,RoleApiPermissions1353,StepCatalog31,FlowDefinition8,FlowStep43,Modes3,Channels3,Topics4,CreditLimitTypes1,RoleModeMappings12,ApplicationModeMappings7 - 🔴 ห้าม copy ข้าม env:
ApiClients6,ApiClientScopes8,ApiKeys7 — เป็น credential ต่อ env ต้อง generate ใหม่ แล้วเอาค่าใหม่ไปใส่ Key Vault[assume]
- copy/seed ได้:
7. วิธี copy data จาก DEV (ถ้าเลือกทางนี้)
มี script ของเดิมเป็นแบบอย่าง: Backend_Iac/scripts/clone-dev-to-sit.sh [repo]
- pattern:
TRUNCATE ... CASCADEทุกตารางใน target →pg_dump --data-only --disable-triggers --no-owner --no-acl <src> | psql <tgt> - 🔴 ข้อบกพร่องของ script ตัวนี้ อย่าใช้ตรง ๆ:
- truncate เฉพาะ
schemaname = 'public'แต่pg_dumpดัมพ์ ทุก schema → schemaConfig,los,Orchestration,File,ApplicationTermsจะโดน insert ซ้ำ → PK ชน[repo] - map ไป
SIT_*ซึ่ง ตอนนี้ไม่มี DBSIT_*อยู่บน server เลยสักตัว และ sourceBffDbที่ script อ้าง ก็ไม่มีแล้ว[live] --data-onlyจะพัง__EFMigrationsHistory(ซ้ำกับที่ migration เพิ่งเขียน) → ต้อง--exclude-table
- truncate เฉพาะ
- ทางที่แนะนำสำหรับ env ใหม่: migration ลง schema ก่อน (§5) แล้ว copy แบบ เลือกตาราง เฉพาะถัง C
# เลือกตารางที่ต้องการจริง ๆ เท่านั้น
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
- credential ของ server อยู่ที่ ../db/tmp.txt — password มี single quote ให้ใส่ไว้ในไฟล์ script อย่า escape บน shell
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 ยังไง
- smoke test จาก pod จริง (ปิด password ก่อนพิมพ์ออกจอเสมอ):
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 เหล่านี้ |