Plan on PR
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”pull request ควรตอบคำถามเดียวก่อนที่ใครจะ approve: การเปลี่ยนแปลงนี้จะทำอะไรกับสาม cloud? ฉะนั้นครึ่งแรกของ CI/CD คือ workflow แบบ read-only ที่บนทุก PR รัน terragrunt run --all plan กับ tree live/ ของแต่ละ cloud แล้วโพสต์ผลกลับเป็น comment — หนึ่งอันต่อ cloud
workflow มีสามส่วนที่ขยับ:
- matrix บน
[aws, gcp, azure]เพื่อให้ job เดียวกันรันสามครั้ง หนึ่งครั้งต่อ cloud แบบขนาน - OIDC-based cloud auth — แต่ละ job แลก short-lived GitHub token เป็น cloud credential ฉะนั้นไม่มี access key ที่อยู่ยาว ๆ เก็บใน repo
- ขั้น plan-and-post ที่รัน
terragrunt run --all planในlive/<cloud>/แล้ววาง output ลงบน PR
พอจบคุณจะได้ .github/workflows/plan.yml ที่เปลี่ยน “เชื่อผมเถอะ ไม่มีอะไรหรอก” ให้เป็น diff ที่คุณอ่านได้
terragrunt plan คือ safety rail ที่ถูกที่สุดเท่าที่เรามี: เพราะแสดง set ที่แน่นอนของ add, change และ destroy ก่อน ที่อะไรจะเกิดขึ้น บน cloud เดียวคุณคงรันด้วยมือ ข้ามสาม cloud การรันด้วยมือคือวิธีที่คุณจะลืมสักตัว — ฉะนั้นเราให้หุ่นยนต์ทำ แบบเดียวกัน ทุกครั้ง บนทุก PR
สองการตัดสินใจกำหนดรูปของ design ข้อแรก plan job เป็น read-only: IAM role ที่ job นี้ assume describe และ read state ได้ แต่ create, modify หรือ destroy ไม่ได้ PR จาก fork หรือ merge ที่แย่ ไม่มีวัน mutate infrastructure จากขั้น plan ได้ — สิ่งที่แย่ที่สุดที่ทำได้คือแสดง plan ให้คุณดู ข้อสอง เราใช้ OIDC แทน stored key GitHub สร้าง token ที่ sign แล้วให้แต่ละ run; แต่ละ cloud เชื่อ token นั้นสำหรับ repo และ role ที่เจาะจง ไม่มีอะไรอยู่ยาว ๆ นั่งรอรั่วใน Secrets
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”One matrix job per cloud vs. one job that loops all three
- Pros: matrix รันสาม cloud แบบขนาน ฉะนั้น PR ได้ feedback ในเวลาของ cloud ที่ช้าที่สุดตัวเดียว ไม่ใช่ผลรวม plan ของแต่ละ cloud เป็น job ที่แยกกัน มี log ของตัวเอง, credential ของตัวเอง และ PR comment ของตัวเอง — เมื่อ AWS แดงและ GCP เขียว ก็เห็นชัดในพริบตา
- Cons: สาม job หมายถึงสาม OIDC trust relationship ที่ต้อง set up และสาม sticky comment บน PR job แบบ loop เดียวจะมี credential เดียวและ comment เดียว แลกกับ runtime แบบ serial และ log ที่พันกัน
OIDC federation vs. long-lived access keys
- Pros: ไม่มี secret ที่ต้อง rotate, รั่ว หรือ scope ผิด credential อยู่ได้แค่ไม่กี่นาทีและผูกกับ repo ของคุณและ (ถ้าต้องการ) branch หรือ PR ที่เจาะจง นี่คือ best practice ปัจจุบันบนทั้งสาม cloud
- Cons: ต้อง wiring ล่วงหน้ามากกว่า — identity provider และ role/service-account ต่อ cloud พร้อม trust condition ที่คุณต้องทำให้ถูกเป๊ะ (
subclaim พิมพ์ผิดง่าย) long-lived key เป็นการ paste ครั้งเดียวลงSecrets; OIDC เป็นโปรเจกต์ IAM เล็ก ๆ
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. dependency blocks need mock outputs for plan
หัวข้อที่มีชื่อว่า “1. dependency blocks need mock outputs for plan”On a fresh PR — before anything has been applied — the cluster unit’s dependency "network" has no real outputs to read, and terragrunt run --all plan would error. The fix (verified against /gruntwork-io/terragrunt) is mock_outputs on the dependency, so plan has placeholder values to work with while apply still uses the real ones:
include "root" { path = find_in_parent_folders("root.hcl") }terraform { source = "../../../modules/aws/cluster" }
dependency "network" { config_path = "../network"
# Used only when network hasn't been applied yet (e.g. plan on a fresh PR). # apply always uses the real outputs. mock_outputs = { network_id = "vpc-mock" private_subnet_ids = ["subnet-mock-a", "subnet-mock-b"] } mock_outputs_allowed_terraform_commands = ["plan", "validate"]}
inputs = { name = "clouddeploy" network_id = dependency.network.outputs.network_id subnet_ids = dependency.network.outputs.private_subnet_ids node_count = 2 node_size = "small"}ถ้าไม่มีตัวนี้ plan บน green-field ล้มเหลวตั้งแต่ PR แรก เมื่อมีตัวนี้ run --all plan เดินผ่าน dependency graph ทั้งหมดและแสดง diff ที่สมจริง
2. .github/workflows/plan.yml
หัวข้อที่มีชื่อว่า “2. .github/workflows/plan.yml”The workflow triggers on PRs to main, grants the id-token: write permission OIDC requires, and fans out over the cloud matrix. Each cloud has its own auth step gated by if: matrix.cloud == '<cloud>', because the three auth actions are genuinely different:
name: plan
on: pull_request: branches: [main]
# Least privilege at the workflow level.permissions: id-token: write # required to request the OIDC JWT contents: read # required for actions/checkout pull-requests: write # required to post the plan comment
jobs: plan: name: plan (${{ matrix.cloud }}) runs-on: ubuntu-latest strategy: fail-fast: false # one cloud failing shouldn't hide the others matrix: cloud: [aws, gcp, azure] steps: - uses: actions/checkout@v6
# --- OIDC cloud auth: one step per cloud, read-only role --- - name: Auth to AWS if: matrix.cloud == 'aws' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ vars.AWS_PLAN_ROLE_ARN }} # read-only role aws-region: us-east-1
- name: Auth to GCP if: matrix.cloud == 'gcp' uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }} service_account: ${{ vars.GCP_PLAN_SA }} # read-only SA
- name: Auth to Azure if: matrix.cloud == 'azure' uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# --- Tooling --- - uses: hashicorp/setup-terraform@v3 with: terraform_wrapper: false # let Terragrunt drive Terraform directly
- name: Install Terragrunt run: | curl -sSL -o /usr/local/bin/terragrunt \ https://github.com/gruntwork-io/terragrunt/releases/download/v0.67.4/terragrunt_linux_amd64 chmod +x /usr/local/bin/terragrunt terragrunt --version
# --- Plan the whole live/<cloud> tree --- - name: terragrunt run --all plan id: plan working-directory: live/${{ matrix.cloud }} run: | { echo '### Terragrunt plan — `'"${{ matrix.cloud }}"'`' echo '' echo '```' terragrunt run --all plan --terragrunt-non-interactive 2>&1 | tail -n 300 echo '```' } > plan.md
- name: Post plan to the PR if: always() # post even when the plan errors, so reviewers see why uses: marocchino/sticky-pull-request-comment@v2 with: header: plan-${{ matrix.cloud }} # one sticky comment per cloud, updated on re-push path: live/${{ matrix.cloud }}/plan.mdมีสองอย่างที่ควรชี้ให้เห็น fail-fast: false กัน AWS ที่แดงไม่ให้ยกเลิก job ของ GCP และ Azure — คุณอยากได้ plan ทั้งสามบน PR ไม่ใช่ failure ตัวแรก และ header ต่อ cloud ของ sticky comment หมายความว่าทุก push ใหม่ update สาม comment เดิม แทนที่จะฝัง PR ด้วยกำแพง plan output ใหม่ทุกครั้ง
3. Pin the tool versions
หัวข้อที่มีชื่อว่า “3. Pin the tool versions”CI drift มีจริง: Terragrunt หรือ Terraform ที่ไม่ pin เปลี่ยน “no changes” ให้กลายเป็น diff ที่ไม่คาดคิดข้ามคืนได้ pin Terragrunt release ในขั้น install (ด้านบน) และ provider version ใน block required_providers ของคุณ แล้ว bump อย่างตั้งใจใน PR แยกของตัวเอง — ที่ workflow นี้เองจะแสดง blast radius ให้คุณดูก่อน merge
หมายเหตุเรื่อง syntax: terragrunt run --all plan คือการสะกดปัจจุบัน (terragrunt run-all plan แบบเก่ายังใช้ได้แต่ deprecated แล้ว) CloudDeploy ใช้ run --all ตลอด; ทั้งสองรูปขับ plan ที่เรียงตาม dependency แบบเดียวกัน
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”Locally, reproduce exactly what CI does — plan the whole AWS tree:
cd live/awsterragrunt run --all plan --terragrunt-non-interactiveExpected output: Terragrunt reports the run order (dependencies first), plans each unit, and warns where it fell back to mock outputs:
INFO The stack at . will be processed in the following order for command plan:Group 1- Module ./networkGroup 2- Module ./cluster- Module ./dataGroup 3- Module ./iam- Module ./platformGroup 4- Module ./shopmicro
WARN [cluster] Config ./network is a dependency that has no outputs, but mock outputs provided and returning those in dependency output....Plan: 34 to add, 0 to change, 0 to destroy.จากนั้นเปิด draft PR ด้วยการเปลี่ยนเล็ก ๆ (bump node_count บน cloud หนึ่ง) แล้วดูแท็บ Actions: สาม job plan (aws|gcp|azure) รันแบบขนาน และภายในหนึ่งถึงสองนาทีมีสาม sticky comment โผล่บน PR — แต่ละอันเป็น terragrunt run --all plan ในกรอบ fence สำหรับ cloud นั้น โดย unit ที่เปลี่ยนแสดง ~ node_count = 2 -> 3 diff นั้น บน PR ก่อน merge คือประเด็นทั้งหมด
ตรวจสอบความเข้าใจ
หัวข้อที่มีชื่อว่า “ตรวจสอบความเข้าใจ”- ทำไม plan job ถึง assume role แบบ read-only และป้องกันอุบัติเหตุประเภทไหน?
- บน green-field PR
terragrunt run --all planจะ error ที่ unitclusterทำไม — และอะไรทำให้mock_outputsเป็นการแก้ที่ถูกต้องแทนที่จะ applynetworkก่อน? fail-fast: falseเปลี่ยนพฤติกรรมของสาม cloud job อย่างไรเมื่อตัวใดตัวหนึ่งล้มเหลว?- บอกสองอย่างที่ OIDC federation ให้คุณแต่ long-lived access key ที่เก็บใน
Secretsไม่ให้
ตอนนี้คุณมี PR gate ที่ plan ทั้งสาม cloud แบบเดียวกันทุกครั้ง: matrix บน [aws, gcp, azure], OIDC auth ไปยัง read-only role ต่อ cloud, terragrunt run --all plan บน tree live/ แต่ละอัน และ plan โพสต์กลับเป็น sticky comment ต่อ cloud mock_outputs บน dependency block ของคุณ (ตั้งไว้ตั้งแต่ Terragrunt & Remote State) คือสิ่งที่ให้ plan บน green-field สำเร็จ และ unit live/ เดียวกันที่ deploy ShopMicro คือสิ่งที่ plan เดินผ่าน
plan ที่อ่านได้เป็นแค่ครึ่งของ pipeline ต่อไปเราเปลี่ยน merge ให้เป็นการเปลี่ยนแปลงจริง: Apply and Deploy → ที่ terragrunt run --all apply จัดหา infrastructure และ roll out Helm release — หลัง approval gate