Initial Guide — APIM: จาก service เปล่า ถึง state ปัจจุบัน
จุดประสงค์: หลัง provision API Management ขึ้นมาแล้ว ต้อง config อะไรต่อ เพื่อให้ request จาก app วิ่งทะลุ APIM เข้า AKS ได้เหมือน state ปัจจุบัน ตรวจสอบกับของจริงเมื่อ: 2026-09-21 — APIM
exim-az-hub-apim(RGexim-azure-nonprd-hub-connectivity, subscriptionEximNonPrdHubSua=9359feea-760c-4f7c-9115-30979c2c9c6d, region southeastasia) ปลายทาง: AKSaks-SupperApp-dev(sub501c116b-…) — อ่านคู่กับ initial-guide-aks.md
การอ่าน tag ท้าย bullet:
[live]— query จาก Azure / ยิง request จริงวันที่ 2026-09-21[repo]— อ่านจาก source ในSuperApp/*(Backend_Iac, Frontend_*, Backend_SentinelGatewayService)[assume]— ยังไม่ได้ verify ต้องเช็กเองก่อนใช้
⚠️ ข้อจำกัดของข้อมูล: account ที่ใช้ตรวจ (
silak@exim.go.th) อ่านMicrosoft.Network/*และ resource group ใน hub subscription ไม่ได้ (AuthorizationFailed) → ทุกอย่างที่เป็น NSG / route table / DNS zone / peering ฝั่ง hub เป็น[assume]ทั้งหมด ต้องถาม owner ของ hub
az network private-dns zone listใน hub sub คืน[]— แปลว่าไม่มีสิทธิ์อ่าน ไม่ได้แปลว่าไม่มี zoneไม่พบ bicep/terraform ที่ provision APIM ตัวนี้ใน repo ที่มีในเครื่อง → ค่าทุกตัวใน §2 มาจาก
az apim show/az restของ service ที่รันอยู่ ไม่ใช่ source of truth
0. ขอบเขต
- ครอบคลุม: ตั้งแต่ APIM provision เสร็จ → network/DNS → named value → global policy → API ต่อ service → client เรียกได้จริง
- ไม่ครอบคลุม: การสร้าง hub VNet / firewall / Cloudflare / AKS เอง (เป็น prerequisite — ดู §1 และ initial-guide-aks.md)
- APIM ตัวเดียวรับทั้ง DEV / UAT (และเคยมี SIT) — แยก environment ด้วย prefix ของ displayName ไม่ได้แยก instance
[live]
1. Prerequisite นอก APIM (ขาดอันใดอันหนึ่ง = request ตาย)
- VNet peering hub ↔ spoke ต้อง
Connected— ฝั่ง spoke เห็น peeringexim-azure-nonprd-hub-peer-hub-to-sua→exim-azure-nonprd-hub-vnetstateConnected[live] - APIM ต้องอยู่ใน VNet โหมด Internal —
virtualNetworkType: Internal, subnet…/exim-azure-nonprd-hub-vnet/subnets/APIM[live] - DNS:
api.sua-az-dev.internal/api.sua-az-uat.internalต้อง resolve ได้จากฝั่ง APIM — global policy ยิง backend ด้วย ชื่อโฮสต์ ไม่ใช่ IP → resolve ไม่ได้ = ทุก API ตาย[live: policy]- พิสูจน์แล้วว่า ทำงานจริง (§9 smoke test ได้ HTTP 200 จาก pod ใน AKS) แต่ กลไกที่ทำให้ resolve ได้ยังไม่ยืนยัน
[assume]— เป็นได้ 2 ทาง ต้องถาม hub owner- Private DNS zone
sua-az-dev.internallink เข้า hub VNet (A record →172.29.88.5= internal LB ของ nginx-internal) - custom DNS server บน hub VNet (ผ่าน NVA
172.29.160.68) ที่มี A record ชื่อนี้
- Private DNS zone
- จาก corp DNS ชื่อนี้ resolve ไม่ได้ (
Non-existent domain) → เป็นชื่อสำหรับใน VNet เท่านั้น[live]
- พิสูจน์แล้วว่า ทำงานจริง (§9 smoke test ได้ HTTP 200 จาก pod ใน AKS) แต่ กลไกที่ทำให้ resolve ได้ยังไม่ยืนยัน
- AKS ingress ต้องพร้อมก่อน —
nginx-internal(LB172.29.88.5) + ingress hostapi.sua-az-dev.internal+ path/api/<service>(/|$)(.*)[repo] - Sentinel Gateway ต้องรันอยู่ — global policy เรียก
POST /sentinel/internal/resolve-sessionทุก request ที่มี cookie; Sentinel ล่ม = APIM ตอบ502 sentinel_unavailableทั้งระบบ[live: policy] - NSG ของ subnet APIM (stv2) — ต้องเปิดอย่างน้อย
[assume — อ่านไม่ได้ ใช้ requirement มาตรฐานของ Azure]- inbound TCP 3443 จาก service tag
ApiManagement(management endpoint — ปิดแล้ว portal update config ไม่ได้ + service degrade) - inbound TCP 6390 จาก
AzureLoadBalancer(health probe) - outbound 443 →
Storage,AzureKeyVault,AzureMonitor(+ 1886), outbound 1433 →Sql
- inbound TCP 3443 จาก service tag
- UDR บน subnet APIM — spoke ใช้ default route
0.0.0.0/0 → 172.29.160.68(NVA); ถ้า subnet APIM โดน route เดียวกัน ต้องมี exemption ให้ traffic control-plane ของ APIM ไม่งั้น service เข้าสถานะ degraded[assume]
2. ค่า service ปัจจุบัน (ใช้เป็น target ตอน provision)
| หัวข้อ | ค่าจริง | tag |
|---|---|---|
| SKU | Developer, capacity 1 (ไม่มี SLA, scale-out, zone) | [live] |
| Platform | stv2 |
[live] |
| VNet mode | Internal — subnet APIM ใน exim-azure-nonprd-hub-vnet |
[live] |
| Private IP | 172.29.166.4 |
[live] |
| Public IP (outbound) | 20.247.201.129 (publicNetworkAccess: Enabled) |
[live] |
| Gateway hostname | exim-az-hub-apim.azure-api.net — BuiltIn cert ตัวเดียว ไม่มี custom domain ใน APIM |
[live] |
| Regional gateway | exim-az-hub-apim-southeastasia-01.regional.azure-api.net |
[live] |
| TLS | SSL3 / TLS1.0 / TLS1.1 / 3DES ปิดหมด ทั้ง client และ backend; HTTP/2 ปิด | [live] |
| Managed identity | identity: null — ไม่มี MI → ดึง cert/secret จาก Key Vault ตรง ๆ ไม่ได้ |
[live] |
| Publisher | exim / pongthepl@exim.go.th |
[live] |
| สร้างเมื่อ | 2026-03-23 (instance ปัจจุบัน 2026-05-01 โดย parichat@fusionsol.com) |
[live] |
สิ่งที่ "ไม่มี" และไม่ต้องสร้าง (state ปัจจุบันไม่ได้ใช้) [live]
- Backend entity = 0 → routing ทำผ่าน
set-backend-serviceใน policy ล้วน ๆ - External cache = 0 →
cache-lookup-value/cache-store-valueใช้ internal cache ของ instance (Developer SKU = หายทุกครั้งที่ restart/scale) - Certificate = 0, Gateway (self-hosted) = 0, API version set = 0
- Product เหลือ default
starter/unlimited— API เกือบทั้งหมดไม่ผูก product (unlimitedมี API เดียว)
3. Named values (ทำก่อน paste global policy ไม่งั้น policy save ไม่ผ่าน)
sentinel-internal-api-key(displayNameSentinel-Internal-API-KEY, secret: true) — policy อ้างเป็น{{Sentinel-Internal-API-KEY}}ส่งเป็น headerX-Internal-Api-Keyไปให้ Sentinel[live]69c3c73546346104f4d056ec(displayNameLogger-Credentials--69c3c73546346104f4d056ed, secret) — instrumentation key ของ logger App Insights (Azure สร้างให้ตอนผูก logger)[live]- ⚠️ ค่า key ต้องตรงกับฝั่ง service — Sentinel อ่าน config
SecuritySettings:InternalAccess:ApiKeyซึ่งมาจาก KV objectNamedev--sentinel-gateway--SecuritySettings--InternalAccess--ApiKey(aliasSecuritySettings__InternalAccess__ApiKey)[repo] - ⚠️ มี named value ตัวเดียวแต่ใช้กับทั้ง dev/uat/sit — policy ส่ง key เดียวกันไปทั้ง 3 backend → Sentinel ทุก env ต้องตั้ง ApiKey ค่าเดียวกัน ไม่งั้น env ที่ key ไม่ตรงจะได้
502 sentinel_unavailableทุก request ที่มี cookie[live: policy]+[assume: ยังไม่ได้เทียบค่าใน KV ทั้ง 3 env]
4. Global policy — หัวใจที่ทำให้ทะลุเข้า AKS
ตั้งที่ All APIs → Inbound processing → </> code editor (scope = service) — API ราย ๆ ตัวมีแค่ <base /> ทั้ง 4 section [live]
ลำดับใน policy จริง (ดูตัวเต็มด้วยคำสั่งใน §9):
CORS —
allow-credentials="true", origin list 19 ตัว (localhost 4200/4202/4203,superapp-dev|sit|uat.exim.go.th,admin-superapp*,fx-superapp*,gateway-dev.exim.go.th,lineonboarding-dev,.pages.dev,eximcustnonprd.ciamlogin.com,login.microsoftonline.com), methods GET/POST/PUT/PATCH/DELETE/OPTIONS, headers, exposeContent-Disposition(ให้ JS อ่านชื่อไฟล์ PDF/XLSX)[live]- ⚠️ origin ห้ามมี
/ท้าย — browser ส่ง Origin ไม่มี slash ถ้าใส่จะ match ไม่ติด → CORS พังเงียบ
- ⚠️ origin ห้ามมี
Block
/sentinel/internal/*จากภายนอก — path มี/sentinel/internal/→403 internal_endpoint_not_publicทันที (กันคนนอกเรียก resolve-session แล้ว mint JWT เอง); facade เรียกเองด้วยsend-requestซึ่งไม่ผ่าน inbound นี้ จึงไม่กระทบ[live]Step 1 — อ่าน identity จาก request
[live]cookie-raw= เฉพาะ cookie ที่ขึ้นต้น.Exim.Auth(กรองให้ cache key เสถียร)request-origin= headerOriginenv-key=context.Api.Name.StartsWith("UAT") ? "uat" : StartsWith("SIT") ? "sit" : "dev"— case-sensitiveis-auth-flow= path มี/sentinel/login,/signin-oidc-,/signout-callback-oidc-,/sentinel/logout→ ข้าม session resolve ส่งตรงไป backend
Step 2 — cache key =
"session:" + SHA256(env + "|" + origin + "|" + cookie)(ผสม env กัน JWT ข้าม env, ผสม origin กัน cross-tenant cache poisoning)[live]Step 2.5 — Sentinel internal URL 3 ทาง →
http://api.sua-az-{uat|sit|dev}.internal[live]Step 3 — session façade (เฉพาะ request ที่มี cookie)
[live]- cache lookup → miss →
send-request POST {sentinel-url}/sentinel/internal/resolve-sessiontimeout 10s แนบCookie,X-Internal-Api-Key,Origin,User-Agent - non-200 →
502 sentinel_unavailable+ correlationId isValid:false→ ไม่แนบ JWT แต่ยังส่งต่อ (ให้ backend ตัดสิน);isValid:trueแต่ไม่มี token →502 no_token_in_response- ได้ token →
cache-store-valueTTL 120 วินาที
- cache lookup → miss →
Step 4 — แนบ
Authorization: Bearer <jwt>ทับของเดิม[live]Step 5 — routing เข้า AKS (จุดที่ "ทะลุ" จริง)
[live]<choose> <when condition="@(context.Api.Name.StartsWith("SIT"))"> <set-backend-service base-url="http://api.sua-az-sit.internal" /></when> <when condition="@(context.Api.Name.StartsWith("UAT"))"> <set-backend-service base-url="http://api.sua-az-uat.internal" /></when> <otherwise> <set-backend-service base-url="http://api.sua-az-dev.internal" /></otherwise> </choose>- เป็น HTTP (พอร์ต 80) ไม่ใช่ HTTPS — TLS terminate ที่ APIM, ขาใน VNet เป็น plaintext
[live] context.Api.Name= displayName ไม่ใช่ api-id → ของจริง UAT ทุกตัวขึ้นต้น"UAT "ตัวใหญ่ (ยืนยันแล้ว)[live]
- เป็น HTTP (พอร์ต 80) ไม่ใช่ HTTPS — TLS terminate ที่ APIM, ขาใน VNet เป็น plaintext
Step 6 — rate limit 3 tier (200/นาที auth, 200 bearer, 30 anon) ยัง comment-out = ปัจจุบัน ไม่มี rate limit เลย
[live]Outbound —
set-header Authorization exists-action="delete"(ไม่ให้ JWT หลุดกลับ browser)[live]
5. สร้าง API ต่อ 1 service
- ปัจจุบันมี 52 API บน instance นี้ (DEV + UAT + ของเบ็ดเตล็ด)
[live] - path convention — DEV =
<service>-api(เช่นuserservice-api,centralized-api), UAT =uat-<service>-api[live] - displayName convention (สำคัญกว่า path) —
DEV <Service> API/UAT <Service> APIเพราะ routing ใน §4 Step 5 อ่าน displayName → เปลี่ยนชื่อ = เปลี่ยน env ปลายทางเงียบ ๆ[live] - operation urlTemplate ต้องเป็น path เต็มของ backend — เช่น
GET /api/user-service/v1/admin/apps/{app_id}/roles(userservice-api มี 100 operation)[live]- URL ที่ client เรียก =
https://<gw>/userservice-api+/api/user-service/v1/… - APIM ตัด path prefix (
userservice-api) ออก แล้วต่อท้าย base-url →http://api.sua-az-dev.internal/api/user-service/v1/…→ ตรงกับ nginx ingress/api/user-service(/|$)(.*)
- URL ที่ client เรียก =
serviceUrl= null ทุก HTTP API และถึงตั้งไว้ก็ ไม่มีผล เพราะset-backend-serviceใน global policy ทับเสมอ[live]subscriptionRequired: falseสำหรับ API ของ app ทั้งหมด → ไม่ต้องใช้Ocp-Apim-Subscription-Key[live]protocols: ["https"](บางตัวเก่ายังมีhttpติดมา:filemanageservice-api,dev-filter-api,dev-log-api)[live]- policy ราย API =
<base />เปล่า — อย่าใส่ logic ราย API ถ้าไม่จำเป็น (import spec ทับจะลบ operation-level policy)[live] - วิธี import spec จาก service จริง — มี runbook พร้อม gate/diff/restore อยู่แล้วที่
docs/superpowers/specs/2026-07-14-apim-uat-spec-sync-design.md(kubectl port-forward→/swagger/v1/swagger.json→az apim api import→ restore field ที่ drift)[repo]
6. WebSocket API (SignalR)
- เป็น API ชนิด
type: websocket3 ตัว:dev-fxorchestrator-ws,notification-websocket,6a265faa…(UAT Notification WebSocket)[live] - ตั้ง
serviceUrlเต็ม ๆ รวม hub path เช่นws://api.sua-az-dev.internal/api/notification-service/hubs/appและ path ของ API ก็รวม hub ด้วย (notification-ws/hubs/app)[live] - protocols =
ws,wss;subscriptionRequired: false[live] - client เรียก
wss://gateway-dev.exim.go.th/notification-ws/hubs/app[repo] set-backend-serviceใน global policy ไม่มีผลกับ WS API — WS ยึดserviceUrlเป็นหลัก[live]- ทดสอบ handshake จริง:
curl -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: …' https://exim-az-hub-apim.azure-api.net/notification-ws/hubs/app - ได้
401 {"error":"missing_auth_cookie","message":"WebSocket requires .Exim.Auth cookie"}= body มาจาก notification-service เอง → request ถึง/hubs/appของ backend จริง (ถ้า base-url ถูกทับจน path หาย จะได้404 Resource not foundของ APIM แทน) - ผลที่ตามมา: ตั้ง
serviceUrlของ WS API ให้ครบทั้ง host + hub path เสมอ เพราะ policy ไม่ช่วย route ให้
- ทดสอบ handshake จริง:
7. Logger / Diagnostics
- logger 2 ตัว
[live]apim-appinsight— typeapplicationInsights, instrumentationKey จาก named value,resourceIdชี้ App Insights ใน subscription อื่น (7c29a5cb-…RGexim-azure-hub-connectivity= ฝั่ง PROD hub) → env ใหม่ต้องมีสิทธิ์ข้าม sub หรือเปลี่ยนไปใช้ของตัวเองazuremonitor— typeazureMonitor(ปลายทางอ่านไม่ได้ด้วยสิทธิ์ปัจจุบัน)
- diagnostic
applicationinsights—alwaysLog: allErrors,httpCorrelationProtocol: Legacy,logClientIp: true, *mask query param ทั้งหมด (Hide) ทั้ง frontend และ backend[live] - ไม่ได้ log body (สอดคล้องกับนโยบาย masking ฝั่ง service)
[live]
8. ทางเข้าของ client (ต้นทาง → APIM)
- โดเมนที่ app ใช้จริง =
https://gateway-dev.exim.go.th— ตั้งเป็นapimGatewayUrl/baseDomainใน environment ของ Frontend_AdminSuperApp และ demo client ของ Sentinel[repo]- REST:
https://gateway-dev.exim.go.th/<api-path>/…(เช่น/sentinel-gateway-api/sentinel/…) - WS:
wss://gateway-dev.exim.go.th/notification-ws/hubs/app - UAT ของ Admin portal ก็ยังชี้
gateway-dev.exim.go.th(แยก env ด้วย pathuat-*ไม่ใช่โดเมน)[repo]
- REST:
gateway-dev.exim.go.thชี้เข้า Cloudflare — CNAME →gateway-dev.exim.go.th.cdn.cloudflare.net(104.18.24.212,104.18.25.212)[live]- ยิงจริงจากเครื่อง corp ผ่านโดเมนนี้ได้ HTTP 200 พร้อม
Set-Cookie: __Host-csrf-sentinel-dev→ Cloudflare → (origin/tunnel) → APIM → AKS ครบสาย[live] - กลไกที่ Cloudflare ส่งต่อเข้า APIM private IP ยังไม่ยืนยัน (Cloudflare Tunnel / reverse proxy ใน VNet / F5 on-prem) — ต้องถาม owner
[assume] - ต้องจัดการ Host / SNI mismatch ที่ชั้นหน้า
[live: hostnameConfigurations]+[assume: วิธีที่ใช้จริง]- APIM มี hostname เดียวคือ
exim-az-hub-apim.azure-api.netใช้ BuiltIn cert ไม่มี custom domain → origin ยื่น cert ของ*.azure-api.netเท่านั้น - ตัวที่อยู่ชั้นหน้า (Cloudflare/origin proxy) ต้องอย่างใดอย่างหนึ่ง: ตั้ง
Host/SNI เป็นexim-az-hub-apim.azure-api.net, หรือปิด origin cert verification, หรือเพิ่ม custom domain + cert ให้ APIM (ซึ่งต้องเปิด managed identity ก่อน — ตอนนี้identity: null)
- APIM มี hostname เดียวคือ
- จากในเครือข่าย corp เรียกตรงได้ — DNS องค์กร resolve
exim-az-hub-apim.azure-api.net→172.29.166.4(private IP)[live]
9. Verification checklist
RG=exim-azure-nonprd-hub-connectivity; APIM=exim-az-hub-apim
SUB=9359feea-760c-4f7c-9115-30979c2c9c6d
az account set -s $SUB
B="https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.ApiManagement/service/$APIM"
# 1. service พร้อม + อยู่ใน VNet โหมด Internal + ได้ private IP
az apim show -g $RG -n $APIM \
--query "{state:provisioningState,vnet:virtualNetworkType,ip:privateIpAddresses,sku:sku.name}"
# 2. DNS: ชื่อ gateway ต้องชี้ private IP (เมื่ออยู่ใน corp/VPN)
nslookup exim-az-hub-apim.azure-api.net # ต้องได้ 172.29.166.4
# 3. named value ครบ
az rest --method get --url "$B/namedValues?api-version=2022-08-01" \
--query "value[].properties.displayName"
# 4. global policy มี set-backend-service ครบ 3 env (ไฟล์มี BOM ต้องใช้ --output-file)
az rest --method get --url "$B/policies/policy?format=rawxml&api-version=2022-08-01" \
--output-file global-policy.json && grep -c "set-backend-service" global-policy.json # ต้องได้ 3
# 5. API + displayName prefix (ตัวกำหนด env routing)
az rest --method get --url "$B/apis?api-version=2022-08-01&\$top=60" \
--query "value[].{id:name,disp:properties.displayName,path:properties.path}" -o tsv
# 6. operation ของ API ต้องเป็น path เต็มของ backend
az rest --method get --url "$B/apis/userservice-api/operations?api-version=2022-08-01" \
--query "value[0:5].properties.urlTemplate" -o tsv
# 7. end-to-end จริง — ต้องได้ 200 + Set-Cookie (ทะลุถึง pod ใน AKS)
curl -sk -i https://exim-az-hub-apim.azure-api.net/sentinel-gateway-api/sentinel/antiforgery-token | head -5
curl -sk -i https://gateway-dev.exim.go.th/sentinel-gateway-api/sentinel/antiforgery-token | head -5
# 8. WebSocket ถึง backend จริง (401 missing_auth_cookie = ดี, 404 ของ APIM = path หาย)
curl -sk -i -N --max-time 10 -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://exim-az-hub-apim.azure-api.net/notification-ws/hubs/app | head -8
ผลรันจริง 2026-09-21: ข้อ 7 ได้ HTTP 200 ทั้งสองทาง (body = antiforgery token, header Set-Cookie: __Host-csrf-sentinel-dev) → peering + DNS + policy + ingress + pod ครบสาย [live]
10. ⚠️ Gotcha ที่เจอจริงบน instance ปัจจุบัน
- 404 จาก APIM ≠ backend ตาย —
GET /userservice-api/api/user-service/healthได้404 {"statusCode":404,"message":"Resource not found"}จาก APIM เพราะ ไม่มี operation ที่ match (spec ที่ import ไม่มี/health) → endpoint ใหม่ใน service เรียกผ่าน APIM ไม่ได้จนกว่าจะ import spec ใหม่[live] - routing ผูกกับ displayName — API ที่ชื่อไม่ขึ้นต้น
UAT/SITตกไป dev เสมอ ตัวที่เข้าข่ายตอนนี้:Tempo UAT,Tempo SIT,Echo API,FX API,new dbd,FxFile API,ThirdParty FX API,FX Rate Service API[live] serviceUrlที่มี path prefix ถูกทิ้ง —tempo-uat(…/tempo),DependencyTrack(…/dtrack),6a5c71d9…(ชี้กลับ APIM ตัวเอง) ตั้ง serviceUrl ไว้ แต่set-backend-serviceทับด้วย base-url เปล่า → path prefix หาย ทำให้ API กลุ่มนี้ น่าจะใช้งานไม่ได้จริง[assume — ยังไม่ได้ยิงทดสอบ]- api-id ไม่ตรงกับ env — UAT หลายตัวยังใช้ id เดิมเป็น
sit-*หรือ GUID (sit-codex-api= UAT Codex API,69cfd71f…= UAT UserService API) เพราะ api-id เปลี่ยนไม่ได้ → อย่าใช้ id เดา env[live] - revision ค้าง —
userservice-api;rev=1ยังอยู่คู่กับuserservice-api(current = rev 2)[live] - 5 API ยังบังคับ subscription key (
echo-api,tempo-dev,tempo-sit,tempo-uat,sit-filter-api= UAT Filter API) → UAT Filter API ได้ 401 ถ้า client ไม่ส่งOcp-Apim-Subscription-Keyต่างจาก API อื่นทั้งหมด[live] - ไม่มี rate limit — policy block ถูก comment ไว้ทั้งก้อน
[live] - ไม่มี external cache — JWT cache 120s อยู่ใน memory ของ instance; Developer SKU restart/scale = cache หายหมด ทุก request วิ่งไป Sentinel พร้อมกัน (thundering herd)
[live] - Developer SKU = ไม่มี SLA — nonprod พอได้ แต่ PROD ต้อง Premium (ตาม
docs/lz/lz.mdระบุ PROD hub = APIM Premium)[live]+[repo] - ไม่มี managed identity → ผูก custom domain ด้วย cert จาก Key Vault ไม่ได้จนกว่าจะเปิด MI + ให้สิทธิ์ KV
[live] - App Insights ของ logger อยู่คนละ subscription (
7c29a5cb-…) — env ใหม่ที่ไม่มีสิทธิ์ sub นั้นผูก logger ไม่ได้[live] - policy ไฟล์มี BOM —
az restที่ไม่ใส่--output-fileพังด้วย'charmap' codec can't encode characterบน Windows[live] - แก้ policy ต้อง export live มา diff ก่อนเสมอ — comment ในไฟล์ policy เองเตือนว่าเคยมี drift (v3/v4/v5 คนละ base)
[live]
11. ลำดับสรุป (TL;DR)
- Network ก่อน — peering hub↔spoke, subnet
APIM, NSG 3443/6390 + outbound, UDR exemption - DNS — ทำให้
api.sua-az-<env>.internalresolve จาก hub VNet ไปที่ internal LB ของ nginx (172.29.88.5) - AKS ต้องพร้อมก่อน — ingress + Sentinel Gateway รันได้ (ไม่งั้น request ที่มี cookie ได้ 502 ทั้งหมด)
- Provision APIM — Internal VNet mode, stv2, ปิด TLS เก่า
- Named values —
sentinel-internal-api-keyให้ตรงกับSecuritySettings:InternalAccess:ApiKeyของ Sentinel ทุก env - Global policy (All APIs scope) — CORS → 403 internal → session façade →
set-backend-service3 ทาง → outbound strip Authorization - สร้าง API ต่อ service — displayName
DEV|UAT <x> API, path<x>-api/uat-<x>-api, import spec จาก/swagger/v1/swagger.json,subscriptionRequired=false, policy<base/> - WebSocket API แยกต่างหาก พร้อม
serviceUrlเต็ม path - Logger + diagnostic (App Insights / Azure Monitor, mask query param)
- หน้าบ้าน — ชี้
gateway-dev.exim.go.th(Cloudflare) เข้า APIM พร้อม rewrite Host เป็นexim-az-hub-apim.azure-api.net - เดิน checklist §9 — smoke test ต้องได้ 200 ทั้งจาก
azure-api.netและจากโดเมนหน้าบ้าน
ลำดับ smoke test ตอน env ใหม่: ยิง request ไม่มี cookie ก่อน (ข้าม resolve-session) เพื่อแยกปัญหา routing ออกจากปัญหา Sentinel — ถ้า path นั้นผ่านแล้วค่อยทดสอบแบบมี cookie
12. คำถามที่ต้องถาม owner ของ hub (ยังค้าง)
api.sua-az-dev.internal/api.sua-az-uat.internalresolve ด้วยอะไร — private DNS zone หรือ custom DNS หลัง NVA172.29.160.68- NSG / UDR ของ subnet
APIMตั้งไว้อย่างไร (rule 3443 / 6390 / outbound service tag) - Cloudflare ส่ง traffic เข้า APIM private IP ผ่านอะไร และ rewrite
Hostที่ชั้นไหน - APIM ตัวนี้ provision ด้วย IaC หรือมือ (ไม่พบ bicep/terraform ใน repo ที่มีในเครื่อง)