ข้ามไปยังเนื้อหา

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 ที่คุณต้องทำให้ถูกเป๊ะ (sub claim พิมพ์ผิดง่าย) long-lived key เป็นการ paste ครั้งเดียวลง Secrets; OIDC เป็นโปรเจกต์ IAM เล็ก ๆ

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:

live/aws/cluster/terragrunt.hcl
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 ที่สมจริง

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 ใหม่ทุกครั้ง

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:

Terminal window
cd live/aws
terragrunt run --all plan --terragrunt-non-interactive

Expected 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 ./network
Group 2
- Module ./cluster
- Module ./data
Group 3
- Module ./iam
- Module ./platform
Group 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 คือประเด็นทั้งหมด

  1. ทำไม plan job ถึง assume role แบบ read-only และป้องกันอุบัติเหตุประเภทไหน?
  2. บน green-field PR terragrunt run --all plan จะ error ที่ unit cluster ทำไม — และอะไรทำให้ mock_outputs เป็นการแก้ที่ถูกต้องแทนที่จะ apply network ก่อน?
  3. fail-fast: false เปลี่ยนพฤติกรรมของสาม cloud job อย่างไรเมื่อตัวใดตัวหนึ่งล้มเหลว?
  4. บอกสองอย่างที่ 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