Initial Guide — CI (Azure DevOps + ACR): จาก service เปล่า ถึง state ปัจจุบัน
จุดประสงค์: หลัง provision Azure DevOps project + Azure Container Registry ขึ้นมาแล้ว ต้อง config อะไรต่อ (service connection / สิทธิ์ / variable group / pipeline) เพื่อให้ pipeline build → scan → push image → patch GitOps manifest ได้เหมือน state ปัจจุบัน ตรวจสอบกับของจริงเมื่อ: 2026-09-25 — ADO org
eximthprojectSuperApp, ACRsuaazcrdev(RGsua-azure-nonprd, subscriptionEximSuperAppDev&SIT=501c116b-493a-452e-a823-c7c0f443d01a) ปลายทาง: GitOps repoBackend_Iac→ ArgoCD → AKSaks-SupperApp-dev— อ่านคู่กับ initial-guide-aks.md
การอ่าน tag ท้าย bullet:
[live]— query จาก ADO / Azure จริงวันที่ 2026-09-25[repo]— อ่านจาก source ในSuperApp/*(branchorigin/developmentของ Backend_Iac, Backend_UserService, Frontend_HostAppSuperApp)[assume]— ยังไม่ได้ verify ต้องเช็กเองก่อนใช้
⚠️ ข้อจำกัดของข้อมูล:
az devopsจากเครื่อง corp โดน Zscaler SSL inspection →CERTIFICATE_VERIFY_FAILEDต้องทำ CA bundle =certifi/cacert.pemของ az CLI +Zscaler Root CA(export จากCert:\LocalMachine\Root) แล้วexport REQUESTS_CA_BUNDLE=<bundle>ก่อนเรียก- Repo permission ของ Build Service บน
Backend_Iacไม่ได้ query ตรง — ยืนยันได้แค่ทางอ้อมว่า push ได้ (มี commitAzure Pipelines [CI]บนdevelopmentวันนี้) → ชื่อ permission ที่ต้องเปิดเป็น[assume]- ไม่พบ IaC (bicep/terraform) ที่สร้าง service connection / variable group → ทุกอย่างสร้างมือผ่าน portal
[live: createdBy]
0. ขอบเขต
- ครอบคลุม: ตั้งแต่ ADO project + ACR มีแล้ว → service connection → identity/RBAC → สิทธิ์ Build Service (feed + GitOps repo) → variable group → pipeline definition → run ผ่าน
- ไม่ครอบคลุม: ArgoCD sync / deploy เข้า AKS (ดู initial-guide-aks.md), การ provision SonarQube / Dependency-Track เอง (อยู่ใน AKS ns
security) - ACR ตัวเดียวใช้ทั้ง DEV / SIT / UAT — แยก env ด้วย prefix ของ image tag (
dev-<sha>/sit-<sha>/uat-<sha>) ไม่ได้แยก registry[repo] - Frontend (
Frontend_*) ไม่ได้ build image — build static แล้ว upload เข้า Storage$webผ่าน service connectionsua-azure-nonprd-rg(สรุปไว้ใน §2, §5)
1. Prerequisite นอก ADO
1.1 ACR suaazcrdev — ค่าปัจจุบัน [live]
| หัวข้อ | ค่าจริง |
|---|---|
| SKU | Premium |
| Login server | suaazcrdev-a2aqcveegedbdbbk.azurecr.io |
| Domain name label scope | Unsecure (Azure ต่อ hash ท้ายชื่อ → -a2aqcveegedbdbbk) |
| Public network access | Enabled, network rule default Allow (hosted agent เข้าได้จาก internet) |
| Admin user | Enabled (ไม่มี pipeline ไหนใช้ — pipeline ใช้ SP) |
| Anonymous pull | ปิด |
| Dedicated data endpoint | ปิด |
| Zone redundancy | ปิด |
| สร้างเมื่อ | 2026-03-19 |
| Repository ปัจจุบัน | ~30 repo เช่น user-service, sentinel-gateway-service, task-service, fx*-service … (image name = SERVICE_NAME ใน pipeline) |
- ⚠️ login server ที่มี hash ถูก hardcode ทั่วระบบ —
ACR_LOGIN_SERVERในทุกpipelines/ci*.yml, default ของresolve-image-tag-*.yml,update-manifest.yml, และimage:ในทุกsrc/yamls/<env>/superapp/deployments/*.yamlของ Backend_Iac[repo]- → ถ้า provision ACR ใหม่แล้ว DNL scope / hash ต่างไป = hostname ใหม่ → ต้องไล่แก้ทุก pipeline + manifest ไม่งั้น push/pull ผิดที่
- อยาก hostname เดิม → ต้องใช้ชื่อ + DNL scope เดิม (hash
Unsecureผูกกับชื่อ, ไม่การันตีว่าได้ค่าเดิมหลังลบทิ้ง[assume])
- ต้องไม่ปิด public access / firewall โดยไม่เตรียม self-hosted agent — pipeline ทั้งหมดรันบน Microsoft-hosted (§6) ซึ่งไม่มี IP คงที่
[assume]
1.2 Managed identity สำหรับ ARM service connection
- UAMI
sua-azure-nonprd-msi(RGsua-azure-nonprd, clientId8187ca15-cd40-49a1-9485-0ec544f12f5d) ต้องมีก่อนสร้าง SCsua-azure-nonprd-rg[live] - RBAC ของ MI นี้
[live]Contributor@ RGsua-azure-nonprdStorage Blob Data Contributor@ RGsua-azure-nonprd(frontend upload$web)
- ⚠️ MI ตัวนี้ใช้ร่วมกับ AKS workload identity ด้วย — มี federated credential
filemanagement-service-fed→system:serviceaccount:superappdev:bff-service-sa[live]→ ห้ามลบ/recreate MI เพื่อแก้ปัญหา pipeline
2. Service connections (Project Settings → Service connections)
2.1 ตัวที่ต้องมี (ใช้งานจริง)
| ชื่อ | Type | Auth | Identity | สิทธิ์ | ใครใช้ |
|---|---|---|---|---|---|
sua-azure-nonprd-acr |
Docker Registry → Azure Container Registry | Service Principal (secret) — creationMode Automatic |
SP eximth-SuperApp-edf6de57-67c4-4b7b-b672-ba31834a35df (appId 0a75a342-383c-4306-bd4d-5e18a6813711) |
AcrPush @ registry suaazcrdev เท่านั้น |
Docker@2 login ใน ci*.yml ทุก backend (~63 จุด) [repo] |
sua-azure-nonprd-rg |
Azure Resource Manager | Workload Identity Federation (MI) — creationMode Automatic, scope Subscription EximSuperAppDev&SIT |
UAMI sua-azure-nonprd-msi |
ตาม §1.2 | AzureCLI@2 ผ่าน $(AZURE_SERVICE_CONNECTION) ใน Frontend_* + QA_E2eTests |
ขั้นตอนสร้าง (ถ้าต้องทำใหม่):
sua-azure-nonprd-acr- New service connection → Docker Registry → Registry type Azure Container Registry → Authentication Service Principal → เลือก subscription
EximSuperAppDev&SIT→ registrysuaazcrdev→ ชื่อsua-azure-nonprd-acr(ชื่อต้องตรงเป๊ะ เพราะ YAML อ้างด้วยชื่อ) - ADO จะสร้าง app registration
eximth-SuperApp-<scId>+ assignAcrPushที่ registry ให้อัตโนมัติ (azureSpnRoleAssignmentIdเก็บไว้ใน SC)[live] - คนสร้างต้องมีสิทธิ์สร้าง app ใน Entra +
User Access Administrator/Ownerที่ registry[assume] - ⚠️ auth แบบ SP + client secret → secret มีวันหมดอายุ → เมื่อหมด
Docker@2 loginตายทุก pipeline; เช็กวันหมดที่ Entra app → Certificates & secrets[assume: ยังไม่ได้เช็กวันหมด]
- New service connection → Docker Registry → Registry type Azure Container Registry → Authentication Service Principal → เลือก subscription
sua-azure-nonprd-rg- New service connection → Azure Resource Manager → Identity type Managed identity (หรือ App registration แบบ WIF แล้วชี้ MI เดิม) → scope Subscription → ชื่อ
sua-azure-nonprd-rg - ต้องมี federated credential บน MI
[live]- issuer
https://login.microsoftonline.com/6ff02f6c-cf97-4a42-9cf9-e27d79c35d3a/v2.0 - subject
/eid1/c/pub/t/bC_wb5fPQkqc-eJ9ecNdOg/a/rISbSSETf0KqFyZ8ppdXmA/sc/5b065406-8439-45c6-a2a7-15e379961866/a739b886-318d-4c39-805c-e08063d74cb2 - (
5b065406…= project id,a739b886…= service connection id)
- issuer
- ⚠️ subject ผูกกับ id ของ SC → ลบ SC แล้วสร้างใหม่ = id ใหม่ → ต้องเพิ่ม federated credential ใหม่บน MI ด้วย ไม่งั้น
AADSTS700213[assume: error code]
- New service connection → Azure Resource Manager → Identity type Managed identity (หรือ App registration แบบ WIF แล้วชี้ MI เดิม) → scope Subscription → ชื่อ
- ทั้งสองตัว: Security → "Grant access permission to all pipelines" = เปิด (state ปัจจุบัน
allPipelines: true)[live]— ปิดเมื่อไหร่ pipeline ใหม่จะค้าง "needs permission" ครั้งแรก
2.2 ตัวที่มีแต่ตายแล้ว — ไม่ต้องสร้างใหม่
sua-acr-dev(Docker Registry, SP appId0e9a606a…) และsua-dev-pipeline-mi-sc(ARM, WIF, MIsua-az-pipeline-mi) — ชี้ subscription7c29a5cb-…"POC - Super App" ซึ่งตอนนี้ตอบSubscriptionNotFound[live]→ candidate ลบทิ้ง- grep ทั้ง
SuperApp/*เจอ YAML อ้างsua-acr-dev3 จุด (นับรวม worktree/สำเนาในเครื่องด้วย — ยังไม่ได้แยกว่าอยู่บน branch จริงกี่จุด)[repo]— ถ้ารันจะ fail; ควรแก้ให้ชี้sua-azure-nonprd-acr
- grep ทั้ง
superapp-acr-connection— ไม่มีอยู่จริง แต่ถูกอ้างในBackend_Iac/pipelines/templates/docker-build-push.ymlและBackend_Iac/scripts/azure-pipelines-docker-build.yml[repo]→ template เก่าที่ไม่มี pipeline ไหนใช้แล้ว (pipeline ปัจจุบัน inlineDocker@2เอง)
3. RBAC ฝั่ง ACR (สรุปเป้าหมาย)
AcrPush→ SP ของ SCsua-azure-nonprd-acr(scope = registry เท่านั้น ไม่ใช่ RG — least privilege ที่ถูกแล้ว)[live]AcrPull→ kubelet identityaks-SupperApp-dev-agentpool(สำหรับ AKS pull — ฝั่ง deploy)[live]- ⚠️ มี
AcrPullอีก 2 ตัว ผูก principal ที่ ถูกลบไปแล้ว (4a45defa…,534bd9b4…—az ad sp showไม่เจอ)[live]→ orphan ลบได้, ไม่ต้องสร้างซ้ำ - ไม่ต้องให้ Build Service ของ ADO มีสิทธิ์ ACR ตรง ๆ — push ผ่าน SC ล้วน ๆ
4. สิทธิ์ของ Build Service identity (SuperApp Build Service (eximth))
Project setting ที่ทำให้ token เป็นแบบ project-scoped [live]:
enforceJobAuthScope: true—$(System.AccessToken)= identitySuperApp Build Service (eximth)ไม่ใช่Project Collection Build ServiceenforceReferencedRepoScopedToken: true— token เข้าถึงได้เฉพาะ repo ที่ประกาศในresources.repositories/checkoutdisableClassicBuildPipelineCreation: true,disableClassicReleasePipelineCreation: true— สร้างได้แค่ YAML pipeline
4.1 Azure Artifacts feed eximth (private NuGet/npm)
- feed
eximthเป็น org-scoped (ไม่ผูก project), upstream: npmjs, NuGet Gallery, PowerShell Gallery, PyPI, Maven Central, …[live] - ใช้ใน build อย่างไร
[repo]nuget.configของ service ชี้https://pkgs.dev.azure.com/eximth/_packaging/eximth/nuget/v3/index.json(sourceSharedUtilityPackage) ใช้ password%FEED_TOKEN%- pipeline ส่ง
FEED_TOKEN: $(System.AccessToken)→docker build --secret id=nuget_token,env=FEED_TOKEN→ DockerfileRUN --mount=type=secret,id=nuget_token … dotnet nuget update source(token ไม่ติด layer) - Frontend ใช้
npmAuthenticate@0กับ feed เดียวกัน
- Feed permission ปัจจุบัน
[live]SuperApp Build Service (eximth)= Collaborator (Feed and Upstream Reader) — API แสดงชื่อเป็นProject Collection Build Service (P5b065406-…)แต่ descriptor คือServiceIdentity;…:Build:5b065406-8439-45c6-a2a7-15e379961866(= project id ของ SuperApp) → เป็น identity ระดับ project ตัวที่$(System.AccessToken)ใช้จริง[SuperApp]\SuperApp Team= Contributor[eximth]\Project Collection Valid Users= Reader
- ✅ grant ที่ต้องทำตอน setup ใหม่: Feed settings → Permissions → เพิ่ม
SuperApp Build Service (eximth)= Feed and Upstream Reader (Collaborator) ไม่ใช่ Reader — Reader เฉย ๆ ดึง package ใหม่จาก upstream (nuget.org/npmjs) ผ่าน feed ครั้งแรกไม่ได้[assume: พฤติกรรมตาม doc ของ Azure Artifacts]
4.2 GitOps repo Backend_Iac (stage UpdateManifest)
- pipeline ประกาศ
resources.repositories: iac(SuperApp/Backend_Iac, refrefs/heads/development) →checkout: iac+persistCredentials: true→git push origin HEAD:<GITOPS_BRANCH>[repo]- ประกาศ resource นี้คือสิ่งที่ทำให้ scoped token เข้าถึง Backend_Iac ได้ — ลบออก = checkout/push 403
- ต้องให้
SuperApp Build Service (eximth)บน repoBackend_Iac[assume: ชื่อ permission]- Contribute = Allow
- ถ้า branch
development/sitมี branch policy → Bypass policies when pushing = Allow
- พิสูจน์ว่าทำงานอยู่: commit
Azure Pipelines [CI] ci(dev): update consent-service to dev-12beb88 [skip ci](2026-09-25 08:46 UTC) บนorigin/development[live] - ⚠️ UAT push ไป branch
developmentไม่ใช่uat— ArgoCD appsuperapp-uatอ่านsrc/yamls/uat/superapp/จาก branchdevelopment(GITOPS_BRANCHในci-orchestrate.yml)[repo]
5. Variable groups (Pipelines → Library)
ทั้งหมดเป็น type Vsts (ค่าเก็บใน ADO) — ไม่มีตัวไหน link Key Vault เพราะ KV ติด firewall ที่ hosted agent เข้าไม่ถึง (comment ใน ci-orchestrate.yml) [live] + [repo]
| Group | ตัวแปร | ใครใช้ |
|---|---|---|
Security-Stack-UAT |
SONAR_TOKEN 🔒, DTRACK_API_KEY 🔒 |
backend ci-orchestrate.yml (Sonar/SBOM stage — ทำงานเฉพาะ branch uat), Frontend |
Frontend_HostAppSuperApp_{DEV,SIT,UAT} |
AZURE_SERVICE_CONNECTION=sua-azure-nonprd-rg, AZURE_STORAGE_ACCOUNT = webhostsuperapp / sitwebhostsuperapp / suastuatwbhost, AZURE_STORAGE_CONTAINER=$web |
Frontend_HostAppSuperApp |
Frontend_AdminSuperApp_{DEV,SIT,UAT} |
เหมือนกัน — storage webadminsuperapp / sitwebadminsuperapp / suastuatwbadmin |
Frontend_AdminSuperApp |
Frontend_RemoteAppFX_{DEV,SIT,UAT} |
เหมือนกัน — storage devwebfxsuperapp / sitwebfxsuperapp / suastuatwbfx |
Frontend_RemoteAppFX |
Frontend_LINE_Connectivity_DEV |
SC sua-azure-nonprd-rg, storage suastdevwblob |
Frontend_LINE_Connectivity |
QA-E2E-Secrets |
BASE_URL, OTP_SOURCE, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD 🔒, SC sua-azure-nonprd-rg, STORAGE_ACCOUNT_NAME, STORAGE_CONTAINER_NAME |
QA_E2eTests |
- ค่า secret (🔒) ต้องขอจาก owner —
SONAR_TOKENสร้างใน SonarQube (https://gateway-dev.exim.go.th/uat-sonar),DTRACK_API_KEYสร้างใน Dependency-Track (https://gateway-dev.exim.go.th/uat-dtrack) - ⚠️ YAML อ้าง group ด้วยชื่อ → ชื่อต้องตรง; และต้องเปิด Pipeline permissions → Open access หรือ authorize ต่อ pipeline
[assume: state ปัจจุบันไม่ได้ query] - Backend build image ได้โดยไม่ต้องมี group อื่นนอกจาก
Security-Stack-UAT(ถ้าไม่มี group นี้ pipeline compile ไม่ผ่านเลย เพราะประกาศไว้ระดับ rootvariables)
6. Pipeline definitions
- สร้างแบบ YAML, existing file ต่อ repo 1 pipeline
[live]- Backend: YAML path
/pipelines/ci-orchestrate.yml(ตัวเดียวคุม dev/sit/uat — triggerdevelopment,sit,uatแล้วเลือก env จากBuild.SourceBranchName)[repo] ci.yml/ci-sit.ymlในแต่ละ repo เป็นรุ่นเก่า (แยก env) — ไม่ได้ผูก pipeline แล้ว[live: yamlFilename ของ pipeline 10, 12]- Frontend:
/pipelines/azure-pipelines.yml(+ มี pipelineSIT_Frontend_*แยกใน folder\Frontend\SIT)
- Backend: YAML path
- Folder/ชื่อ pipeline = ชื่อ repo
[live]\Backend\Platform— UserService, SentinelGateway, TaskService, Codex, Consent, FileManagement, Notification, Orchestrator, ThirdParty, Workflow, Centralized, Log, Filter, CloudflareSecurityLog\Backend\FX— ThirdPartyFX, FX_Contract, FxOrchestrator, FX_Rate, FX_Codex, FX_File, FX_Report\Frontend\DEV,\Frontend\SIT— Host / Admin / RemoteAppFX
- Agent: pool
Azure Pipelines(Microsoft-hosted)vmImage: ubuntu-latest— ไม่มี self-hosted pool ที่ใช้งาน (Defaultว่าง)[live]+[repo] resources.repositories.iacpinnedrefs/heads/developmentเพราะ template@iacresolve ตอน compile → ทุก env ใช้ template จาก branchdevelopment[repo]- Default branch ของ pipeline backend =
masterแต่ไม่มีผล — trigger มาจาก YAML[live]
7. สิ่งที่ pipeline ทำตอนรัน (เพื่อรู้ว่าต้องเปิด egress / สิทธิ์อะไร)
ci-orchestrate.yml ของ backend (ตัวอย่าง Backend_UserService) [repo]
- SecretScan — gitleaks (โหลดจาก
github.com/gitleaks/…,exitCode: 0= warn only) - Test —
UseDotNet@2(.NET 10) +docker run postgres:17/redis:7.4บน agent →dotnet restore --configfile nuget.config(retry 3 รอบ เพราะ feed upstream เคย 503) →dotnet test; บนuatห่อด้วย SonarQube begin/end (SONAR_TOKEN) - SecurityScan (uat เท่านั้น) — CycloneDX SBOM → POST ไป Dependency-Track (
DTRACK_API_KEY),continueOnError - BuildAndPush
checkout: self+checkout: iac- overlay
Backend_Iac/config/<svc>/<env>/appsettings.jsonทับappsettings.<Env>.json(มี config-parity guard — key หายจะ fail build) Docker@2 loginด้วยsua-azure-nonprd-acr- resolve tag
dev-|sit-|uat-+ short SHA (หรือ semver ถ้าใส่imageVersion) DOCKER_BUILDKIT=1 docker build --file Dockerfile.{Dev,SIT,UAT} --secret id=nuget_token …- Trivy scan (โหลด installer จาก
raw.githubusercontent.com,exitCode: 0= warn only) docker push suaazcrdev-a2aqcveegedbdbbk.azurecr.io/<svc>:<tag>
- UpdateManifest —
sedแก้image:ในsrc/yamls/<env>/superapp/deployments/<x>-deployment.yaml→ commit[skip ci]→ push (retry 5 + rebase) → Checkov scan (softFail)
Egress ที่ hosted agent ต้องออกได้: *.azurecr.io, pkgs.dev.azure.com, mcr.microsoft.com, Docker Hub (postgres, redis), github.com / raw.githubusercontent.com, gateway-dev.exim.go.th (Sonar/DT) — hosted agent อยู่นอก corp จึงไม่ติด Zscaler แต่ Dockerfile ก็ COPY ZscalerRootCertificate-2048-SHA256.crt ไว้เผื่อ build บนเครื่อง dev [repo]
8. Checklist ลำดับการ setup (ถ้าเริ่มจากศูนย์)
- ACR
suaazcrdevPremium, public access Allow, ได้ login server ที่ตรงกับที่ hardcode (§1.1) - UAMI
sua-azure-nonprd-msi+ RBAC Contributor / Storage Blob Data Contributor @ RG (§1.2) - SC
sua-azure-nonprd-acr(Docker Registry/ACR, SP) → เช็กว่า SP ได้AcrPush@ registry (§2.1) - SC
sua-azure-nonprd-rg(ARM, WIF → MI) → เช็ก federated credential subject ตรง SC id (§2.1) - เปิด "Grant access to all pipelines" ทั้ง 2 SC
- Project settings:
Limit job authorization scopeเปิด (ค่าปัจจุบัน) (§4) - Feed
eximth:SuperApp Build Service (eximth)= Feed and Upstream Reader (Collaborator) (§4.1) - Repo
Backend_Iac:SuperApp Build Service (eximth)= Contribute (+ Bypass policies ถ้ามี branch policy) (§4.2) - Variable group
Security-Stack-UAT(+ Frontend_* / QA ถ้าต้องการ) (§5) - สร้าง pipeline ต่อ repo →
/pipelines/ci-orchestrate.ymlใน folder ตาม §6 - AKS kubelet identity ได้
AcrPull(ส่งต่อ initial-guide-aks.md) (§3)
9. Smoke test — state ปัจจุบันยืนยันว่าทำงาน [live]
Backend_UserServicerun บนdevelopment= succeeded (2026-09-25 08:02 UTC)Backend_SentinelGatewayServicerun บนdevelopment= succeeded (2026-09-25 08:26 UTC)Frontend_HostAppSuperApprun บนdevelopment= partiallySucceeded (2026-09-25 08:20 UTC — มี step warn-only)- Backend_Iac
origin/developmentมี commit จากAzure Pipelines [CI]วันนี้ ≥ 3 ตัว (consent, codex, fxorchestrator) → push image + GitOps patch ครบวง
วิธีเช็กเองหลัง setup:
# (เครื่อง corp) ตั้ง CA bundle ก่อน — ดูหมายเหตุหัวเอกสาร
export REQUESTS_CA_BUNDLE=<path-to-bundle-with-zscaler-root>
az devops service-endpoint list --org https://dev.azure.com/eximth -p SuperApp -o table
az pipelines runs list --org https://dev.azure.com/eximth -p SuperApp --pipeline-ids 12 --top 3 -o table
az acr repository show-tags -n suaazcrdev --repository user-service --orderby time_desc --top 5 -o tsv