Skip to main content

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

แทน 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