Membangun pipeline CI/CD untuk Terraform yang aman: prinsip larangan apply dari laptop, alur PR yang menjalankan fmt, tflint, security scan, dan plan, hingga approval gate sebelum apply dengan autentikasi cloud via OIDC di GitHub Actions dan GitLab CI.

Setelah di episode 12 sebelumnya kita membahas bagaimana Terragrunt merapikan arsitektur multi-environment hingga tinggal satu perintah run-all apply — pada episode kali ini kita akan membahas bagian yang tidak kalah penting: bagaimana eksekusi itu harus dijalankan secara aman dan otomatis melalui pipeline CI/CD.
Di episode 11, kalian sudah punya gudang tools kualitas: terraform fmt, terraform validate, tflint, Checkov, dan Terratest. Semua tools itu hanya berguna jika dijalankan secara konsisten di setiap perubahan kode. Kalau dijalankan manual dari laptop masing-masing engineer, maka hasilnya bergantung mood, mesin, dan kredensial si penulis kode — itulah resep insiden.
Mengapa topik ini krusial di dunia kerja nyata? Karena salah satu pelajaran paling pahit di tim infrastruktur adalah: terraform apply yang dilakukan dari laptop developer terhadap environment produksi adalah akar dari hampir semua insiden IaC. Kredensial admin tersimpan di laptop, tidak ada audit trail yang lengkap, tidak ada code review yang efektif, dan satu perintah salah ketik bisa menghapus produksi. Di episode ini, kalian akan belajar membangun pipeline yang menjadikan produksi tidak bisa disentuh dari laptop siapa pun — hanya dari pipeline yang terkontrol, ter-audit, dan terkunci.
Mari kita mulai dari aturan paling fundamental dalam CI/CD IaC, yang sering disebut sebagai the golden rule of Terraform security:
Important
Aturan emas: Kredensial untuk environment produksi tidak boleh ada di laptop developer. Semua perubahan ke produksi harus melewati pipeline CI/CD yang terkontrol — dengan code review, otomatis plan, dan approval gate manusia sebelum apply. Developer hanya bekerja di environment non-produksi, atau bahkan hanya menunggu hasil plan dari pipeline.
Kenapa aturan ini begitu tegas? Karena ada tiga masalah yang otomatis hilang ketika kredensial produksi tidak ada di laptop:
| Masalah | Dengan Laptop Credential | Dengan Pipeline + OIDC |
|---|---|---|
| Audit trail | Tidak ada jejak siapa apply kapan | Setiap run tercatat: commit, user, timestamp |
| Serangan/pencurian laptop | Kredensial produksi ikut bocor | Tidak ada kredensial permanen yang bisa dicuri |
| Error manusia | Satu salah ketik = produksi down | Plan + review + approval sebelum eksekusi |
| Prinsip least privilege | Developer memegang akses penuh | Role IAM spesifik per environment & per job |
Dari tabel di atas, kunci utamanya adalah OIDC (OpenID Connect) — mekanisme autentikasi passwordless di mana CI/CD (misal GitHub Actions) mendapat token yang valid dalam waktu singkat untuk meng-asumsikan sebuah role IAM di cloud, tanpa menyimpan Access Key statis di mana pun. Kita akan membahas implementasinya sebentar lagi.
Sebelum menulis YAML, kalian harus paham dulu pola alurnya. Pipeline Terraform di tim profesional selalu dibagi menjadi dua jalur besar:
| Tahap | Trigger | Tool | Tujuan |
|---|---|---|---|
| 1. Formatting | Pull Request | terraform fmt -check | Memastikan gaya kode seragam |
| 2. Validation | Pull Request | terraform validate | Memastikan sintaks & referensi valid |
| 3. Linting | Pull Request | tflint | Mendeteksi error spesifik provider |
| 4. Security Scan | Pull Request | checkov / trivy | Mendeteksi miskonfigurasi keamanan |
| 5. Plan | Pull Request | terraform plan | Pratinjau perubahan + posting ke PR |
| 6. Apply | Merge ke main | terraform apply | Eksekusi nyata, dengan approval gate |
Pola di atas menjawab pertanyaan penting: kenapa plan dilakukan di PR, bukan setelah merge? Karena plan yang diposting ke komentar PR memberi reviewer kemampuan untuk menilai dampak perubahan sebelum menyetujuinya. Bayangkan reviewer melihat plan yang akan menambahkan 3 security group dan menghapus 1 database endpoint — semua bisa dikoreksi sebelum berbahaya.
Di sisi lain, apply baru berjalan setelah merge ke main, dan di GitHub Actions hal itu dikunci melalui environment protection rules (environment production) yang mewajibkan manual approval dari orang yang berwenang. Inilah approval gate yang dimaksud.
Mari kita mulai implementasinya. Workflow pertama berjalan setiap ada Pull Request. Ia menjalankan fmt, validate, tflint, checkov, dan plan — lalu memposting hasil plan ke komentar PR:
name: Terraform Plan
on:
pull_request:
paths:
- "infrastructure/**"
branches:
- main
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infrastructure/dev
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.5
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gh-oidc-terraform-dev
aws-region: ap-southeast-1
- name: Terraform fmt
run: terraform fmt -check -recursive
- name: Terraform validate
run: terraform validate
- name: Setup tflint
uses: terraform-linters/setup-tflint@v4
- name: Run tflint
run: tflint --chdir=infrastructure/dev
- name: Checkov security scan
uses: bridgecrewio/checkov-action@master
with:
directory: infrastructure/dev
skip_check: CKV_AWS_169
- name: Terraform Plan
id: plan
run: terraform plan -no-color -input=false
- name: Comment plan on PR
uses: thollander/actions-comment-pull-request@v2
with:
message: |
## Terraform Plan Result
```diff
${{ steps.plan.outputs.stdout }}
Mari kita bedah bagian-bagian yang paling penting:
- **`permissions: id-token: write`** — mengizinkan job meminta token OIDC. Ini adalah fondasi autentikasi tanpa password. Tanpa baris ini, `configure-aws-credentials` akan gagal.
- **`pull-requests: write`** — dibutuhkan agar job bisa menulis komentar plan ke PR.
- **`role-to-assume`** — ARN role IAM yang di-asumsikan job ini. Role khusus environment `dev`, jadi plan hanya punya akses terbatas, bukan akses penuh produksi.
- **`terraform plan -no-color`** — output tanpa kode warna supaya rapi ketika ditulis ke komentar PR.
- **`id: plan`** — memberi label pada step. `setup-terraform` membungkus CLI sehingga `stdout`-nya bisa dibaca lewat `steps.plan.outputs.stdout` dan disisipkan ke komentar.
<Callout type="tip" descriptionColor="normal">
Satu perangkap umum: format output `terraform plan` menggunakan notasi warna ANSI dan karakter unicode yang bisa merusak tampilan komentar PR. Selalu gunakan `-no-color`, dan jika output terlalu panjang (ratusan baris), potong di batas tertentu di dalam script komentar — banyak tim menambahkan logika *truncate* agar komentar PR tidak menjadi raksasa.
</Callout>
### OIDC: Login Cloud Tanpa Kredensial Statis
Bagian yang membuat pipeline ini aman adalah **OIDC**. Alurnya kurang lebih seperti ini:
```bash icon="iBash" title="Alur autentikasi OIDC (konseptual)"
Developer push commit
│
▼
GitHub Actions meminta ID token ke GitHub OIDC provider
│ (token berisi claim: repo, ref, environment, job)
▼
AWS STS: assume role dengan WebIdentity (role gh-oidc-terraform-dev)
│ (AWS memvalidasi token + claim policy)
▼
Job menerima kredensial sementara (valid ~1 jam, tanpa secret tersimpan)Di sisi cloud, tim IAM membuat role yang bisa di-asumsikan hanya oleh token OIDC dari repo & environment tertentu. Contoh trust policy role gh-oidc-terraform-dev:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012: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:myorg/infrastructure-live:*"
}
}
}
]
}Yang menarik: sub bisa dipersempit ke branch, environment, bahkan tag tertentu. Misalnya repo:myorg/infra:ref:refs/heads/main — artinya role produksi hanya bisa diasumsikan ketika pipeline berjalan di branch main, dan tidak pernah dari laptop siapa pun. Inilah yang membuat "apply dari laptop" menjadi tidak mungkin secara teknis, bukan sekadar peraturan.
Warning
Jangan pernah menyimpan AWS Access Key statis di GitHub Secrets (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY). Meski cara lama itu masih berfungsi, ia mengunci kalian pada kredensial yang bisa bocor dan tidak bisa dirotasi otomatis. OIDC adalah standar industri saat ini: kredensial bersifat ephemeral, ter-scope per job, dan tidak pernah tertulis di mana pun. Untuk provider cloud lain, konsepnya sama: google-github-actions/auth untuk GCP, azure/login untuk Azure.
Workflow kedua berjalan ketika kode di-merge ke main. Inilah approval gate: environment production dikonfigurasi dengan required reviewers di pengaturan repository, sehingga terraform apply tidak pernah berjalan tanpa persetujuan manusia yang berwenang:
name: Terraform Apply
on:
push:
branches:
- main
paths:
- "infrastructure/**"
permissions:
id-token: write
contents: read
jobs:
apply:
runs-on: ubuntu-latest
environment: production
concurrency: terraform-production
defaults:
run:
working-directory: infrastructure/prod
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.5
- name: Configure AWS Credentials (OIDC) for Production
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gh-oidc-terraform-prod
aws-region: ap-southeast-1
- name: Terraform Init
run: terraform init -input=false
- name: Terraform Apply
run: terraform apply -auto-approve -input=falseTiga hal yang perlu diperhatikan:
environment: production — ketika job menunjuk environment yang memiliki protection rules (required reviewers), GitHub akan menahan job sampai reviewer menyetujui. Inilah manual approval gate yang membuat apply tidak bisa jalan otomatis tanpa manusia.concurrency: terraform-production — mencegah dua apply produksi berjalan bersamaan. Ingat pelajaran state locking di episode 5: tanpa ini, dua pipeline yang berjalan paralel bisa saling menimpa state.-auto-approve — otomatis menyetujui prompt konfirmasi terraform apply, karena persetujuan manusia sudah diambil melalui environment protection, bukan di terminal.Note
Pola lain yang umum: memisahkan langkah plan dan apply dalam satu workflow merge, di mana plan berjalan tanpa approval lalu hasilnya dipakai apply dengan approval. Keduanya valid; yang terpenting adalah setiap apply produksi selalu melewati persetujuan manusia dan state locking yang benar.
Di tim yang menggunakan GitLab, konsepnya identik — hanya sintaksnya berbeda. Pipeline GitLab memakai stages (mirip jobs di GitHub) dan environments (untuk approval). Contoh .gitlab-ci.yml:
stages:
- validate
- plan
- apply
variables:
TF_ROOT: infrastructure/dev
AWS_REGION: ap-southeast-1
.tf_base: &tf_base
image:
name: hashicorp/terraform:1.9.5
entrypoint: [""]
before_script:
- terraform --version
fmt-validate:
<<: *tf_base
stage: validate
script:
- cd $TF_ROOT
- terraform fmt -check -recursive
- terraform validate
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
plan:
<<: *tf_base
stage: plan
script:
- cd $TF_ROOT
- terraform init
- terraform plan -no-color -out=plan.tfplan
artifacts:
paths:
- $TF_ROOT/plan.tfplan
expire_in: 7 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
apply:
<<: *tf_base
stage: apply
environment:
name: production
script:
- cd $TF_ROOT
- terraform apply plan.tfplan
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manualPerbedaan utama dengan GitHub Actions:
when: manual pada job apply adalah bentuk approval gate GitLab — job tidak berjalan sampai engineer menekan tombol "play" pada pipeline.environment: name: production memungkinkan GitLab mencatat deployment history dan menampilkan status environment di UI.plan.tfplan) memastikan file plan yang dibuat di tahap plan dipakai persis sama di tahap apply — tidak ada perbedaan antara apa yang di-plan dan apa yang di-apply.Important
Gunakan file plan (terraform plan -out=plan.tfplan) alih-alih meng-apply ulang dari nol. Dengan plan.tfplan, perubahan yang di-review di tahap plan dijamin sama persis dengan yang dieksekusi di tahap apply — menghilangkan risiko "plan bilang A, apply ternyata B" yang bisa terjadi jika ada drift di antara keduanya.
Untuk konfigurasi yang setara, perbandingan keduanya bisa dilihat di sini:
# Plan saat PR
on:
pull_request:
paths: ["infrastructure/**"]
# Apply saat merge ke main
on:
push:
branches: [main]
paths: ["infrastructure/**"]
# Approval gate lewat environment protection
jobs:
apply:
runs-on: ubuntu-latest
environment: productionTabel ringkas perbandingannya:
| Aspek | GitHub Actions | GitLab CI |
|---|---|---|
| Trigger PR | on: pull_request | rules + $CI_PIPELINE_SOURCE |
| Approval gate | Environment protection (required reviewers) | when: manual |
| State locking antar run | concurrency | Environment deployment_tier / lock |
| Plan → Apply konsistensi | Simpan plan file sebagai artifact | Artifacts + terraform plan -out |
| OIDC ke AWS | configure-aws-credentials | id_tokens + aws cli / oidc config |
Terakhir, mari kita bicara tentang jebakan yang paling sering membuat pipeline IaC bermasalah:
| Kesalahan | Gejala | Solusi |
|---|---|---|
| Kredensial produksi di laptop / secret statis | Bocor saat laptop dicuri, sulit dirotasi | OIDC + role per environment |
Lupa id-token: write | Job configure-aws-credentials error 401 | Tambahkan permission OIDC di job |
| Apply tanpa approval gate | Perubahan bisa terjadi tanpa review | Environment protection / when: manual |
Tidak ada concurrency | Dua apply paralel menimpa state | Tambahkan concurrency key |
Tidak pakai -no-color di plan | Komentar PR berantakan karakter ANSI | Gunakan -no-color |
| Secrets di print ke log | Kredensial tampil di output pipeline | Gunakan masking, sensitive = true, jangan echo |
| Plan dan apply tidak pakai file yang sama | Apa yang di-plan beda dengan yang di-apply | Gunakan -out=plan.tfplan sebagai artifact |
| Pipeline hanya dijalankan manual | Kualitas tools tidak terpakai | Pastikan PR selalu memicu pipeline |
Warning
Jangan pernah menulis perintah terraform apply -auto-approve di workflow yang dipicu oleh PR tanpa environment protection. Kombinasi -auto-approve + trigger tanpa review adalah resep bencana: satu commit jahat atau salah bisa langsung mengubah produksi tanpa ada yang mengetahuinya sampai semuanya rusak. Auto-approve hanya aman jika persetujuan manusia sudah diambil di layer lain (environment protection).
Pada episode 13 ini kita telah membahas bagaimana membangun pipeline CI/CD yang aman untuk Terraform. Inti dari episode ini adalah satu prinsip: terraform apply untuk produksi tidak pernah berasal dari laptop siapa pun — ia hanya bisa terjadi melalui pipeline yang terkontrol, dengan plan yang diposting untuk direview di PR, dan apply yang terkunci oleh approval gate serta autentikasi OIDC yang passwordless.
Poin kunci yang perlu kalian bawa pulang:
fmt → validate → tflint → security scan → plan → post komentar PR.apply via OIDC, dengan concurrency untuk mencegah bentrok state.when: manual + environment sebagai approval gate; konsepnya sama, sintaksnya beda.-no-color untuk output plan dan simpan plan.tfplan sebagai artifact agar plan dan apply konsisten.Di episode 14 selanjutnya, kita akan membahas bagaimana sebagian dari pekerjaan yang barusan kita bangun manual bisa diambil alih oleh platform khusus: Managed IaC Platforms (Terraform Cloud / Spacelift) — membahas fitur remote execution, VCS integration, state management terkelola, private module registry, dan policy enforcement, serta kapan kalian harus pindah ke platform seperti ini. Pastikan tetap semangat!