การ apply Terraform จากเครื่อง engineer เป็นขั้นแรกที่ทำได้เร็ว แต่เมื่อโปรเจกต์โตขึ้น ทีมขยายใหญ่ และต้องผ่าน audit จะเริ่มเห็นข้อจำกัด — ไม่มี audit log ครบ, concurrent apply ชนกัน, credential หลุดได้ง่าย — workshop นี้จัดทำ CI/CD pipeline สำหรับ infrastructure deployment ที่ auto plan บน PR, apply เมื่อ merge และมี approval gate สำหรับ prod
ใช้ GitHub Actions + OIDC federation (ไม่ใช้ long-lived access key), backend S3 state, และ DigitalOcean Provider เป็นตัวอย่าง — pattern นี้ portable ใช้กับ GitLab CI, CircleCI, Jenkins ได้เช่นกันเพียงเปลี่ยน syntax
สถาปัตยกรรม Pipeline
Flow:
Developer → PR opened → workflow "plan" runs
├─ terraform fmt -check
├─ terraform validate
├─ terraform plan → post plan เป็น PR comment
└─ tfsec / checkov scan → comment security warning
Reviewer approves + merge → workflow "apply" runs (branch = main)
├─ apply dev อัตโนมัติ
├─ apply staging อัตโนมัติ
└─ apply prod → รอ manual approval → apply
Schedule nightly → workflow "drift-detect"
└─ plan ทุก env เทียบกับ state → แจ้งเตือนถ้า drift
ขั้นที่ 1 — ตั้งค่า OIDC Federation
แทนที่จะเก็บ access key ของ cloud provider เป็น secret ใน GitHub ให้ใช้ OIDC federation — GitHub Actions ออก token ระยะสั้นที่ cloud provider เชื่อถือได้โดยตรง ลดความเสี่ยง credential leak
# AWS: สร้าง OIDC provider ใน IAM
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
# สร้าง IAM Role ที่ trust OIDC provider
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
}
}
}]
}
EOF
aws iam create-role --role-name gh-terraform-apply \
--assume-role-policy-document file://trust-policy.json
ขั้นที่ 2 — Workflow: Plan บน Pull Request
# .github/workflows/tf-plan.yml
name: Terraform Plan
on:
pull_request:
paths: ['envs/**', 'modules/**']
permissions:
id-token: write # สำคัญ: OIDC ต้องใช้
contents: read
pull-requests: write # ให้ post comment plan ลง PR ได้
jobs:
plan:
strategy:
matrix:
env: [dev, staging, prod]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-terraform-apply
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Format & Validate
working-directory: envs/${{ matrix.env }}
run: |
terraform fmt -check -recursive
terraform init -backend=false
terraform validate
- name: Plan
working-directory: envs/${{ matrix.env }}
run: |
terraform init
terraform plan -out=tfplan -no-color | tee plan-${{ matrix.env }}.txt
- name: Comment Plan on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync(
`envs/${{ matrix.env }}/plan-${{ matrix.env }}.txt`, 'utf8'
);
const truncated = plan.length > 50000
? plan.slice(0, 50000) + '\n... (truncated)'
: plan;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '### Terraform Plan: `${{ matrix.env }}`\n```\n' + truncated + '\n```'
});
ขั้นที่ 3 — Workflow: Apply บน Merge
# .github/workflows/tf-apply.yml
name: Terraform Apply
on:
push:
branches: [main]
paths: ['envs/**', 'modules/**']
permissions:
id-token: write
contents: read
jobs:
apply-dev:
runs-on: ubuntu-latest
environment: dev # GitHub Environment
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-terraform-apply
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
- name: Apply dev
working-directory: envs/dev
run: |
terraform init
terraform apply -auto-approve
apply-staging:
needs: apply-dev
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-terraform-apply
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
- name: Apply staging
working-directory: envs/staging
run: |
terraform init
terraform apply -auto-approve
apply-prod:
needs: apply-staging
runs-on: ubuntu-latest
environment: prod # environment นี้ตั้ง required reviewer ใน GitHub UI
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-terraform-apply
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
- name: Apply prod
working-directory: envs/prod
run: |
terraform init
terraform apply -auto-approve
กุญแจของความปลอดภัยอยู่ที่ environment: prod — ใน GitHub settings ของ repository ตั้งค่า Environment “prod” ให้ต้องมี required reviewer (อย่างน้อย 1 คนที่ไม่ใช่ผู้ push) — pipeline จะหยุดรอ approval ก่อน apply
ขั้นที่ 4 — Security Scan ใน PR
# เพิ่มใน tf-plan.yml เป็นอีก job
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: tfsec
uses: aquasecurity/[email protected]
with:
soft_fail: true # ยังให้ PR ผ่านได้ แต่ comment warning
- name: checkov
uses: bridgecrewio/checkov-action@master
with:
directory: envs/
quiet: true
soft_fail: true
soft_fail=true ให้ scan เตือนเฉย ๆ ไม่บล็อก merge ในช่วงแรก เมื่อทีมชินแล้วค่อยเปลี่ยนเป็น hard fail เพื่อบังคับ config ให้ผ่าน security policy เสมอ
ขั้นที่ 5 — Drift Detection แบบ Schedule
# .github/workflows/tf-drift.yml
name: Terraform Drift Detection
on:
schedule:
- cron: '0 2 * * *' # ทุกวันตี 2
workflow_dispatch: # รัน manual ได้
permissions:
id-token: write
contents: read
jobs:
drift:
strategy:
matrix:
env: [dev, staging, prod]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-terraform-readonly
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
- name: Plan
id: plan
working-directory: envs/${{ matrix.env }}
run: |
terraform init
terraform plan -detailed-exitcode -no-color > drift.txt
echo "exit_code=$?" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Alert on drift
if: steps.plan.outputs.exit_code == '2'
run: |
curl -X POST $SLACK_WEBHOOK \
-H 'Content-Type: application/json' \
-d "{\"text\": \"drift detected in ${{ matrix.env }}\"}"
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
terraform plan -detailed-exitcode จะ return 0 = no changes, 1 = error, 2 = diff detected — ใช้ exit code 2 ส่ง alert ไป Slack เพื่อให้ทีมรู้ว่ามีคน manual เปลี่ยน resource บน console (drift)
ขั้นที่ 6 — State Locking กันชนกัน
# envs/dev/backend.tf (และ staging, prod)
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "envs/dev/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "tfstate-lock" # DynamoDB สำหรับ lock
encrypt = true
}
}
หากมี 2 PR merge ชนกัน (race condition) backend lock จะบังคับให้ workflow ที่ 2 รอ — ไม่เกิด state corruption เพราะ concurrent apply
ขั้นที่ 7 — Rollback Strategy
- Git revert + apply — revert commit ที่ผิด push ขึ้น main → pipeline รัน apply → กลับสู่ state เดิม (ใช้ได้ถ้า resource ลบ/สร้างใหม่ได้)
- Terraform state rollback — S3 bucket เปิด versioning → นำ state file version ก่อนหน้ามา restore ผ่าน
aws s3api copy-object --version-id ... - Forward fix — สำหรับ resource ที่ลบแล้วข้อมูลหาย (เช่น database) ควร fix forward ด้วยการ restore จาก backup ไม่ใช่ revert Terraform
ขั้นที่ 8 — Observability ของ Pipeline
- บันทึก
planoutput ทุกรอบเป็น artifact เก็บ 90 วัน เพื่อ audit - ส่ง apply log ไปยัง central log (CloudWatch, Loki) — ดูผ่านกลางได้
- เพิ่ม metric “time to apply” ต่อ environment — ถ้าเกิน threshold (เช่น prod apply เกิน 10 นาที) แจ้งเตือน
- track จำนวน PR ที่มี plan changes vs merge — ถ้าบาง PR เปลี่ยนหลาย resource ต้อง review ละเอียดขึ้น
ข้อควรระวัง
- ห้าม commit secret ใน
.tfหรือ.tfvars— ใช้ GitHub Secrets หรือ Vault + data source - Role OIDC ต้องจำกัด sub claim ให้ตรง repo และ branch จริง ๆ มิฉะนั้น repo อื่นใน org จะ assume role ได้
- ห้ามให้ workflow รัน apply จาก branch ใดก็ได้ — จำกัดเฉพาะ main หรือ release/* เท่านั้น
- Plan output ใน PR comment อาจมีข้อมูล sensitive (เช่น ARN, IP) — ระวังกรณี repo เป็น public
- การ apply ตาม schedule โดยไม่มี human review เสี่ยงต่อการเปลี่ยนแปลงที่ไม่คาดคิด — drift detection ควรเป็น alert เท่านั้น ไม่ auto-apply
สรุป
Pipeline CI/CD สำหรับ Terraform ที่ดีต้องมีอย่างน้อย plan-on-PR, apply-on-merge, approval gate สำหรับ prod และ drift detection — ใช้ OIDC federation แทน access key ยาว และ state lock ผ่าน DynamoDB เพื่อกัน concurrent apply
โครงสร้างนี้ scale ได้ตั้งแต่ทีมเล็ก 2-3 คนไปจนถึง organization ขนาดใหญ่ที่มีหลาย service — เมื่อซับซ้อนมากขึ้นค่อยเพิ่ม self-hosted runner, custom action สำหรับ policy check หรือ integrate Terraform Cloud/Enterprise — แต่เริ่มจาก pattern ง่าย ๆ ที่ได้ audit log + security gate ครบก่อนเสมอ

