การรัน Terraform ด้วยมือจากเครื่อง developer แต่ละคนเป็นวิธีที่ไม่ยั่งยืนเมื่อทีมเริ่มขยาย ปัญหาที่ตามมาคือ state drift, credential หลุดบนเครื่องพนักงาน, ไม่มี audit trail, และ apply ทับกันจนเกิด conflict การนำ Terraform เข้าสู่ pipeline CI/CD จึงเป็นการยกระดับ Infrastructure as Code ให้มีมาตรฐานเทียบเท่า application code ทั่วไป คือทุก change ต้องผ่าน review, automated check, และ deploy ผ่านระบบเดียวกันทั้งทีม
บทความนี้อธิบายสถาปัตยกรรม pipeline ที่เหมาะกับ IaC พร้อมตัวอย่างจริงบน GitHub Actions, GitLab CI, Jenkins และเทคนิคสำคัญอย่าง OIDC authentication, plan artifact review, manual approval gate, และ rollback strategy
สถาปัตยกรรม Pipeline สำหรับ Terraform
Pipeline มาตรฐานสำหรับ Terraform แบ่งเป็น 2 phase หลักที่ trigger คนละเหตุการณ์ Pull Request phase (PR stage) ทำหน้าที่ validate โค้ดและแสดง plan ให้ reviewer เห็นผลกระทบก่อน merge ส่วน Main branch phase (Apply stage) จะรัน apply หลัง PR ถูก merge เข้า main แล้วเท่านั้น แยกสิทธิ์และ trigger ชัดเจน ช่วยป้องกันการ apply โดยไม่ได้ตั้งใจ
- PR Phase: fmt check → validate → lint (TFLint) → security scan (Checkov/tfsec) → plan → comment plan กลับ PR
- Apply Phase: plan อีกครั้ง → (manual approval สำหรับ prod) → apply → save state → notify
- Scheduled Phase (optional): drift detection รายวัน รัน plan แล้วแจ้งเตือนถ้ามี diff ที่ไม่ได้เกิดจาก code
ตัวอย่าง GitHub Actions
GitHub Actions เป็น CI ที่ integrate กับ GitHub repository ได้โดยตรง ตัวอย่าง workflow แยกเป็น 2 ไฟล์เพื่อให้จัดการสิทธิ์แยกกันได้
# .github/workflows/terraform-pr.yml
name: Terraform Plan (PR)
on:
pull_request:
paths:
- 'infra/**'
permissions:
id-token: write # สำคัญ: OIDC ต้อง write
contents: read
pull-requests: write
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- run: terraform fmt -check -recursive
working-directory: infra
- run: terraform init
working-directory: infra
- run: terraform validate
working-directory: infra
- run: terraform plan -no-color -out=tfplan
working-directory: infra
- run: terraform show -no-color tfplan > plan.txt
working-directory: infra
- uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('infra/plan.txt', 'utf8');
const body = `### Terraform Plan\n\`\`\`\n${plan.slice(0, 60000)}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
ส่วน apply workflow จะ trigger เฉพาะตอน push เข้า main branch และใช้ environment ที่ตั้ง protection rule ไว้ ทำให้ต้องมีการ approve ก่อนจริงๆ
# .github/workflows/terraform-apply.yml
name: Terraform Apply (main)
on:
push:
branches: [main]
paths:
- 'infra/**'
permissions:
id-token: write
contents: read
jobs:
apply:
runs-on: ubuntu-latest
environment: production # protection rule + required reviewers
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
aws-region: ap-southeast-1
- uses: hashicorp/setup-terraform@v3
- run: terraform init
working-directory: infra
- run: terraform apply -auto-approve
working-directory: infra
OIDC Authentication (ไม่ต้องเก็บ Access Key)
จุดสำคัญของ pipeline คือ credential ที่ใช้เข้าถึง cloud provider แบบเดิมที่เก็บ static access key เป็น secret ใน CI เป็นวิธีที่เสี่ยง ถ้า secret หลุดคือใช้งานได้ไม่หมดอายุ วิธีที่ปลอดภัยกว่าคือใช้ OIDC (OpenID Connect) ให้ CI runner ขอ token ชั่วคราวจาก cloud provider โดยอิงกับ identity ของ job นั้น ไม่ต้องเก็บ key ใดๆ
# ฝั่ง AWS: สร้าง OIDC provider + IAM Role
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "terraform_ci" {
name = "terraform-ci"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/infra:ref:refs/heads/main"
}
}
}]
})
}
Condition จำกัดว่า role นี้ assume ได้เฉพาะจาก repo และ branch ที่กำหนด ช่วยกัน privilege escalation ถ้ามีคนสร้าง repo ใหม่ชื่อคล้ายกัน หรือ branch ที่ไม่ได้ protect ก็ assume ไม่ได้
ตัวอย่าง GitLab CI
# .gitlab-ci.yml
stages: [validate, plan, apply]
variables:
TF_ROOT: infra
TF_STATE_NAME: ${CI_PROJECT_NAME}
image: hashicorp/terraform:1.7
before_script:
- cd $TF_ROOT
- terraform init
validate:
stage: validate
script:
- terraform fmt -check
- terraform validate
plan:
stage: plan
script:
- terraform plan -out=tfplan
artifacts:
paths: ["$TF_ROOT/tfplan"]
expire_in: 1 week
apply:
stage: apply
script:
- terraform apply tfplan
dependencies: [plan]
when: manual # ต้องกด play เท่านั้น
only:
refs: [main]
ข้อดีของ GitLab คือมี managed backend ให้ใช้เก็บ state ผ่าน HTTP backend และ when: manual ทำให้ apply stage ต้องกด play ก่อนถึงจะรัน เหมาะสำหรับ environment ที่ sensitive
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
options { timestamps() }
environment {
AWS_DEFAULT_REGION = 'ap-southeast-1'
}
stages {
stage('Init & Validate') {
steps {
sh 'terraform init'
sh 'terraform fmt -check'
sh 'terraform validate'
}
}
stage('Plan') {
steps {
sh 'terraform plan -out=tfplan'
archiveArtifacts artifacts: 'tfplan'
}
}
stage('Approval') {
when { branch 'main' }
steps {
input message: 'Apply changes to production?', ok: 'Apply'
}
}
stage('Apply') {
when { branch 'main' }
steps {
sh 'terraform apply tfplan'
}
}
}
}
Plan Artifact กับ Review Process
หัวใจของ IaC pipeline คือการให้ reviewer เห็น plan ก่อน apply เสมอ เทคนิคที่นิยมใช้มี 3 แบบ แบบแรกคือ save plan เป็น binary file (-out=tfplan) แล้วใช้ไฟล์เดียวกันใน apply เพื่อให้แน่ใจว่าสิ่งที่ reviewer เห็นกับสิ่งที่ execute เป็นอันเดียวกัน แบบที่สองคือ parse plan เป็น JSON (terraform show -json tfplan) เพื่อใช้กับ policy check หรือ compliance scan แบบที่สามคือ render plan เป็น markdown comment กลับไปยัง PR ให้ reviewer อ่านได้ใน UI เดียวกับ code review
# Parse plan เป็น JSON เพื่อ automation
terraform show -json tfplan | jq '.resource_changes[] | {address, actions: .change.actions}'
# ตัวอย่าง output
# { "address": "aws_instance.web", "actions": ["create"] }
# { "address": "aws_s3_bucket.old", "actions": ["delete"] }
Secret Management ใน Pipeline
แม้จะใช้ OIDC แล้ว ก็ยังมี secret บางตัวที่ต้องส่งเข้า Terraform เช่น database password, API key ของบริการภายนอก วิธีที่แนะนำคือเก็บใน secret manager (AWS Secrets Manager, HashiCorp Vault, GitHub Encrypted Secrets) แล้วให้ pipeline inject เข้ามาเป็น environment variable ตอนรัน ห้ามเขียน secret ตรงๆ ใน workflow file หรือ print ค่าออกใน log
# GitHub Actions: ส่ง secret เป็น TF_VAR_
- run: terraform apply -auto-approve
env:
TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
TF_VAR_api_token: ${{ secrets.API_TOKEN }}
# ห้ามเขียนแบบนี้ (ถูก log)
- run: echo "password=${{ secrets.DB_PASSWORD }}"
Manual Approval Gate สำหรับ Production
สำหรับ environment ที่สำคัญ ควรมีคน approve ก่อน apply แยกบทบาท developer ที่ merge code กับ operator ที่อนุมัติ deploy GitHub Actions ใช้ Environment Protection Rules, GitLab ใช้ when: manual, Jenkins ใช้ input step ทุกแพลตฟอร์มรองรับการตั้ง required reviewer และ timeout หลัง approve
- ตั้ง required reviewer อย่างน้อย 2 คน สำหรับ prod
- Dev environment ให้ apply อัตโนมัติได้ เพื่อให้ feedback loop เร็ว
- ตั้ง timeout ไม่ให้ pending ค้างนานเกินไป (เช่น 24 ชั่วโมง)
- เก็บ audit log ว่าใครเป็นคน approve พร้อม timestamp
Drift Detection Schedule
Drift คือสถานะที่ cloud resource ถูกแก้ไขนอก Terraform เช่นคนเข้าไปแก้ใน console โดยตรง วิธีจับคือตั้ง scheduled job รัน terraform plan รายวัน ถ้าผลลัพธ์ไม่ใช่ No changes ก็แจ้ง Slack/email ให้ทีมทราบ
# .github/workflows/drift-detect.yml
on:
schedule:
- cron: '0 2 * * *' # 09:00 เวลาไทย
jobs:
detect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
working-directory: infra
- id: plan
run: |
terraform plan -detailed-exitcode -out=tfplan || echo "exit=$?" >> $GITHUB_OUTPUT
working-directory: infra
- if: steps.plan.outputs.exit == '2'
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"⚠️ Infrastructure drift detected"}' \
${{ secrets.SLACK_WEBHOOK }}
Flag -detailed-exitcode จะ return 0 (no change), 1 (error), 2 (มี diff) ทำให้แยกกรณี drift ออกจาก error จริงได้
Rollback Strategy
Terraform เป็น declarative ไม่มี rollback command โดยตรง การย้อนกลับคือ revert code ใน git แล้ว apply ใหม่ วิธีที่ช่วยให้ rollback ทำได้จริงประกอบด้วย เก็บ state version ใน backend (S3 versioning + DynamoDB lock, Terraform Cloud) เพื่อกู้ state เดิมได้ถ้าจำเป็น, ใช้ git tag ทุก release เพื่อ checkout โค้ดกลับจุดที่ต้องการได้ทันที, และแยก change ที่เป็น destructive (drop database, delete volume) ไว้ใน PR ต่างหาก เพื่อให้ reviewer โฟกัสเป็นพิเศษ
- Tag ทุก apply สำเร็จเป็น
deploy-YYYYMMDD-HHMMเพื่อ reference - เก็บ plan output และ apply log อย่างน้อย 90 วันสำหรับ audit
- ทดสอบ rollback workflow ใน staging เป็นระยะ
Terraform Cloud/Enterprise Remote Run
ถ้าไม่อยากสร้าง pipeline เอง Terraform Cloud (TFC) มี remote execution ในตัว เขียน backend เป็น cloud {} แล้วทุกคำสั่ง plan/apply จะวิ่งใน worker ของ HashiCorp มี UI แสดง plan, approval flow, notification, cost estimation, และ policy (Sentinel/OPA) พร้อมใช้ ไม่ต้องเขียน YAML เอง
terraform {
cloud {
organization = "my-org"
workspaces {
name = "production"
}
}
}
เหมาะกับทีมที่ไม่ต้องการ maintain CI เอง แต่มีค่าใช้จ่ายตาม resource หรือจำนวน run ต่อเดือน การตัดสินใจระหว่าง self-hosted CI กับ TFC ขึ้นกับขนาดทีม กับงบ และระดับการ integrate ที่ต้องการ
ข้อควรระวังในการสร้าง Pipeline
- อย่าใช้ static credential: ใช้ OIDC หรือ short-lived token เสมอ
- อย่า apply parallel: state lock จะ conflict ใช้ concurrency rule จำกัด 1 run ต่อ workspace
- อย่า print state/plan ออก log: อาจมีข้อมูล sensitive รั่ว ใช้
-no-colorใน CI แต่อย่าechoplan ตรงๆ - Version pinning: ทั้ง Terraform version และ provider version ต้องถูก pin ไว้ ไม่งั้น CI กับ local ผลต่างกัน
- Auto-approve เฉพาะ non-prod: prod ต้องผ่านคนเสมอ
- Monitor CI runner IAM: permission ที่ให้ runner ควรเป็น least privilege และ review ทุกไตรมาส
สรุป
การนำ Terraform เข้าสู่ pipeline CI/CD เปลี่ยน IaC จากเครื่องมือใช้งาน ad-hoc เป็นกระบวนการ deploy ที่มีมาตรฐาน ทุก change ถูก version control, review, ผ่าน automated test, และ apply โดย identity ที่เราควบคุมได้ โครงสร้าง 2 phase (PR plan + main apply) ร่วมกับ OIDC, manual approval สำหรับ prod, และ scheduled drift detection คือ pattern ที่ผ่านการทดสอบในองค์กรจริงมาแล้ว
ไม่ว่าจะเลือก GitHub Actions, GitLab CI, Jenkins หรือ Terraform Cloud หลักการเดียวกันคือ separation of duties ระหว่าง developer กับ operator, no static secret ใน pipeline, plan ต้อง review ได้ก่อน apply, และมี audit trail ทุก deploy สำหรับทีมที่เพิ่งเริ่มต้น ให้เริ่มจาก workflow แบบง่ายบน single environment แล้วค่อยขยายเป็น multi-env, manual gate, และ drift detection ตามลำดับ

