GitHub Actions
ตั้งค่า CI/CD pipeline สำหรับ Terraform บน GitHub Actions — plan ใน PR, apply ใน main
ทำไมต้อง CI/CD ใน Terraform?
- 🤖 Automated — ไม่ต้อง apply manual จาก laptop
- 👀 Code Review — plan แสดงใน PR ให้ reviewer ดู
- 🔒 Centralized credentials — ไม่ต้องส่ง AWS key ให้ทุกคน
- 📋 Audit trail — ใครเป็นคน apply, เมื่อไหร่
- ✅ Consistency — ใช้ Terraform version + workflow เดียวกันทุก deploy
Basic Workflow
Plan on PR
.github/workflows/terraform-plan.yml
name: Terraform Plan
on:
pull_request:
paths:
- "**.tf"
- "**.tfvars"
- ".github/workflows/terraform-*.yml"
jobs:
plan:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ap-southeast-1
- name: Format Check
run: terraform fmt -check -recursive
- name: Init
run: terraform init
- name: Validate
run: terraform validate
- name: Plan
id: plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const output = require('child_process')
.execSync('terraform show -no-color tfplan')
.toString();
const body = `### Terraform Plan 📋
<details><summary>Show plan output</summary>
\`\`\`hcl
${output}
\`\`\`
</details>
*Triggered by: @${{ github.actor }}*`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
- name: Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
Apply on Main
.github/workflows/terraform-apply.yml
name: Terraform Apply
on:
push:
branches: [main]
paths:
- "**.tf"
- "**.tfvars"
jobs:
apply:
runs-on: ubuntu-latest
environment: production # require approval (optional)
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ap-southeast-1
- name: Init
run: terraform init
- name: Apply
run: terraform apply -auto-approve
OIDC Authentication (Recommended)
แทน access key — ใช้ OIDC ให้ GitHub ขอ AWS temporary credentials:
1. สร้าง IAM OIDC Provider
iam-oidc.tf
data "tls_certificate" "github" {
url = "https://token.actions.githubusercontent.com"
}
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
}
2. สร้าง IAM Role
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsTerraform"
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/myrepo:*"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "github_actions" {
role = aws_iam_role.github_actions.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" # หรือจำกัดกว่านี้
}
3. ใช้ใน GitHub Actions
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsTerraform
aws-region: ap-southeast-1
→ ไม่ต้องเก็บ AWS access key ใน GitHub secrets
Required Permissions ใน Workflow
permissions:
id-token: write # for OIDC
contents: read # checkout
pull-requests: write # comment on PR
Multi-Environment Pattern
.github/workflows/terraform.yml
name: Terraform
on:
pull_request:
paths: ["envs/**"]
push:
branches: [main]
paths: ["envs/**"]
jobs:
plan:
if: github.event_name == 'pull_request'
strategy:
matrix:
env: [dev, staging, prod]
runs-on: ubuntu-latest
defaults:
run:
working-directory: envs/${{ matrix.env }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Plan ${{ matrix.env }}
run: |
terraform init
terraform plan
apply-dev:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
defaults:
run:
working-directory: envs/dev
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: |
terraform init
terraform apply -auto-approve
apply-prod:
needs: apply-dev
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # require approval
defaults:
run:
working-directory: envs/prod
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: |
terraform init
terraform apply -auto-approve
Reusable Workflows
.github/workflows/terraform-reusable.yml
name: Reusable Terraform
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
action:
required: true
type: string # plan or apply
secrets:
AWS_ROLE_ARN:
required: true
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-southeast-1
- run: terraform init
- name: Run ${{ inputs.action }}
run: terraform ${{ inputs.action }} -auto-approve
ใช้:
jobs:
apply-prod:
uses: ./.github/workflows/terraform-reusable.yml
with:
working-directory: envs/prod
action: apply
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
Cache Plugins
- name: Cache Terraform Plugins
uses: actions/cache@v4
with:
path: ~/.terraform.d/plugin-cache
key: terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
- name: Setup
uses: hashicorp/setup-terraform@v3
- run: |
mkdir -p ~/.terraform.d/plugin-cache
echo 'plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"' > ~/.terraformrc
terraform init
→ ลดเวลา init จาก 30s → 5s
Run Linting + Security Scan
- name: TFLint
uses: terraform-linters/setup-tflint@v4
- run: tflint --recursive
- name: Checkov Security Scan
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: terraform
Notify Slack on Apply
- name: Apply
id: apply
run: terraform apply -auto-approve
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "${{ job.status == 'success' && '✅' || '❌' }} Terraform Apply: ${{ github.repository }} on ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Drift Detection (Scheduled)
on:
schedule:
- cron: '0 9 * * *' # daily 9 AM
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: |
terraform init
terraform plan -detailed-exitcode -refresh-only
continue-on-error: true
id: drift
- name: Notify if drift
if: steps.drift.outcome == 'failure'
run: echo "Drift detected — notify team"
Best Practices
✅ DO:
- ใช้ OIDC แทน access key
- Plan in PR, apply on merge to main
- Require approval for prod (environment protection)
- Cache provider plugins
- Format check + validate + tflint + security scan
- Drift detection แบบ scheduled
❌ DON'T:
- ห้าม commit AWS credentials
- ห้ามใช้ -auto-approve ใน prod โดยไม่ approval
- ห้ามให้ทุกคน trigger workflow apply ได้
- ห้าม skip validate/lint
ตัวอย่าง: Production-Ready Workflow
ดู Atlantis — tool ที่จัดการ Terraform PR workflow ใน CI/CD
หรือใช้ Terraform Cloud — managed solution
สรุป
- Plan in PR — comment plan output ให้ reviewer
- Apply on main — auto-deploy after merge
- OIDC > access keys — temporary credentials, no secrets
- Environment protection — require approval สำหรับ prod
- Cache plugins — เร่ง CI
- Drift detection — scheduled job ตรวจ config drift
ต่อไป → CircleCI