Terraform IaC dengan Claude Code: AWS module, state, CI, dan review plan
Panduan Terraform IaC dengan Claude Code di AWS: module, backend, state, CI, policy, secrets, dan kesalahan umum.
Terraform membuat infrastruktur bisa ditulis sebagai kode, tetapi kode yang berhasil dibuat belum tentu aman untuk produksi. VPC yang terlihat rapi bisa saja menyimpan state di laptop, mencampur dev dan prod pada backend key yang sama, membuka subnet publik tanpa sengaja, atau menyembunyikan replace resource di terraform plan. Claude Code sangat membantu, asalkan dipakai sebagai partner implementasi dan review, bukan tombol otomatis untuk apply.
IaC berarti Infrastructure as Code: resource cloud ditulis dalam file yang bisa direview dan disimpan di version control. HCL adalah bahasa konfigurasi Terraform. Module adalah komponen infrastruktur yang bisa dipakai ulang. State adalah catatan Terraform tentang hubungan kode dan resource nyata. Backend adalah tempat state disimpan. Pemahaman ini penting karena insiden Terraform biasanya terjadi di batas antara kode, state, permission, dan review manusia.
Untuk artikel ini, Masa mencoba alur kecil di repository AWS test. Claude Code cepat dalam memecah VPC menjadi module, menata variables, menambahkan validation, dan menulis draft CI. Namun prompt tetap harus tegas: jangan taruh secrets di tfvars, jangan approve plan yang punya destroy, dan jangan pakai pola locking DynamoDB lama untuk backend S3 baru.
Alur kerja dan use case
Alur aman dimulai dari requirement, lalu HCL draft, Terraform checks, plan review, dan apply setelah disetujui.
flowchart LR
A["Tulis requirement"] --> B["Claude Code membuat HCL draft"]
B --> C["terraform fmt / validate"]
C --> D["terraform plan"]
D --> E["Review AI dan manusia"]
E --> F["Apply yang disetujui"]
Use case yang sering muncul:
| Use case | Yang bisa dibuat Claude Code | Yang harus dicek manusia |
|---|---|---|
| Network AWS baru | VPC, subnets, routes, tags | CIDR overlap dan biaya NAT Gateway |
| Desain module | modules/vpc, variables, outputs | Batas tanggung jawab module |
| Pemisahan environment | envs/dev.tfvars, envs/prod.tfvars | Backend key dan permission prod |
| CI dan policy | fmt, validate, plan, JSON checks | destroy, replace, IAM melebar |
Dasar produk ada diClaude Code docs. Konsep module ada diTerraform Modules docs. Untuk permission AWS, lanjutkan ke panduan internalAWS IAM.
Prompt Claude Code dengan batas aman
Jangan hanya meminta “buatkan Terraform”. Jelaskan scope, hal yang dilarang, dan command verifikasi.
claude -p "
Buat Terraform hanya untuk AWS VPC module.
Jangan hapus file yang sudah ada kecuali saya minta secara eksplisit.
Requirement:
- Asumsikan Terraform 1.10 atau lebih baru
- Gunakan provider hashicorp/aws
- Buat VPC, 2 public subnets, 2 private subnets, Internet Gateway, NAT Gateway
- Simpan kode reusable di modules/vpc
- Pisahkan dev dan prod dengan tfvars
- Jangan tulis secrets atau AWS access keys di tfvars
- Sertakan terraform fmt -recursive, terraform validate, dan terraform plan
- Jika plan berisi destroy atau replacement, anggap belum disetujui
"
Bagian paling penting adalah “jangan hapus”, “tanpa secrets”, dan “destroy belum disetujui”. Claude Code akan mengisi detail yang kosong dengan pola umum. Untuk Terraform, pola umum bisa berarti biaya tambahan, jaringan terbuka, atau IAM yang terlalu luas.
Struktur Terraform AWS yang bisa dipakai
Struktur berikut cukup kecil untuk memulai. Dengan AWS credentials dan bucket state yang benar, kamu bisa menjalankan format, validate, dan plan. Pada Juni 2026, backend S3 Terraform mendukung locking dengan use_lockfile; locking berbasis DynamoDB sudah ditandai deprecated di dokumentasi resmi. Selalu cekS3 backend resmi sebelum menyalin tutorial lama.
infra/
versions.tf
variables.tf
main.tf
envs/dev.tfvars
modules/vpc/main.tf
modules/vpc/variables.tf
modules/vpc/outputs.tf
# versions.tf
terraform {
required_version = ">= 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 7.0"
}
}
backend "s3" {
bucket = "replace-with-your-tfstate-bucket"
key = "claude-code-terraform-iac/dev/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
use_lockfile = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = var.common_tags
}
}
# variables.tf
variable "aws_region" {
type = string
description = "AWS region to deploy into."
default = "ap-southeast-1"
}
variable "environment" {
type = string
description = "Environment name such as dev, staging, or prod."
}
variable "common_tags" {
type = map(string)
description = "Tags applied to every supported AWS resource."
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the VPC."
}
variable "public_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
}
variable "private_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
}
# main.tf
module "network" {
source = "./modules/vpc"
project = "claude-code-iac"
environment = var.environment
vpc_cidr = var.vpc_cidr
public_subnets = var.public_subnets
private_subnets = var.private_subnets
common_tags = var.common_tags
}
Desain module dan variables
VPC module sebaiknya hanya menangani network. Jika ALB, ECS, RDS, dan IAM ikut masuk, plan menjadi besar dan sulit direview. Untuk resource yang bertahan lama, gunakan for_each dengan nama stabil daripada bergantung pada count.index.
# modules/vpc/variables.tf
variable "project" {
type = string
description = "Project name used in resource names."
}
variable "environment" {
type = string
description = "Environment name used in tags and names."
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the VPC."
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid CIDR block."
}
}
variable "public_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
validation {
condition = length(var.public_subnets) >= 2 && alltrue([for s in var.public_subnets : can(cidrhost(s.cidr, 0))])
error_message = "Define at least two valid public subnet CIDR blocks."
}
}
variable "private_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
validation {
condition = length(var.private_subnets) >= 2 && alltrue([for s in var.private_subnets : can(cidrhost(s.cidr, 0))])
error_message = "Define at least two valid private subnet CIDR blocks."
}
}
variable "common_tags" {
type = map(string)
description = "Common tags."
}
# modules/vpc/main.tf
locals {
public_subnets_by_name = { for subnet in var.public_subnets : subnet.name => subnet }
private_subnets_by_name = { for subnet in var.private_subnets : subnet.name => subnet }
name_prefix = "${var.project}-${var.environment}"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-vpc"
})
}
resource "aws_subnet" "public" {
for_each = local.public_subnets_by_name
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
map_public_ip_on_launch = true
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-public-${each.key}"
Tier = "public"
})
}
resource "aws_subnet" "private" {
for_each = local.private_subnets_by_name
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-private-${each.key}"
Tier = "private"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = values(aws_subnet.public)[0].id
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
}
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
Backend, state, dan pemisahan environment
State bukan cache biasa. Di dalamnya ada resource IDs dan kadang atribut sensitif. Gunakan bucket S3 dengan versioning, locking, permission ketat, dan backend key berbeda untuk setiap environment.
# envs/dev.tfvars
aws_region = "ap-southeast-1"
environment = "dev"
vpc_cidr = "10.40.0.0/16"
common_tags = {
Project = "claude-code-iac"
Environment = "dev"
Owner = "masa"
ManagedBy = "terraform"
}
public_subnets = [
{ name = "a", cidr = "10.40.0.0/24", az = "ap-southeast-1a" },
{ name = "b", cidr = "10.40.1.0/24", az = "ap-southeast-1b" }
]
private_subnets = [
{ name = "a", cidr = "10.40.10.0/24", az = "ap-southeast-1a" },
{ name = "b", cidr = "10.40.11.0/24", az = "ap-southeast-1b" }
]
Untuk production, ubah CIDR, tags, dan backend key. Workspace bisa dipakai, tetapi tfvars eksplisit lebih mudah direview oleh tim yang baru mulai.
Verifikasi dengan fmt, validate, dan plan
Dokumentasi resmi Terraform menjelaskan fmt, validate, dan plan. Mulai dari validasi tanpa remote backend.
cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate
Lalu inisialisasi backend nyata dan simpan plan.
terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt
Minta Claude Code review yang fokus ke risiko.
claude -p "
Review plan.txt.
Prioritaskan destroy, replace, force replacement, perluasan IAM, public exposure, dan perubahan backend key.
Jika ada delete, jangan approve plan.
Tulis pertanyaan yang harus dijawab reviewer manusia.
"
CI, policy, permission, dan secrets
Di GitHub Actions, hindari AWS access keys jangka panjang. Gunakan OIDC untuk assume IAM role yang sempit; panduan resminya ada diGitHub Actions OIDC in AWS. Role tersebut sebaiknya mengikutiAWS IAM best practices.
name: terraform-plan
on:
pull_request:
paths:
- "infra/**"
permissions:
contents: read
id-token: write
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.10.0"
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-terraform-plan
aws-region: ap-southeast-1
- run: terraform init -backend=false
- run: terraform fmt -check -recursive
- run: terraform validate
- run: terraform init -reconfigure
- run: terraform plan -var-file=envs/dev.tfvars -out=tfplan
- run: terraform show -json tfplan > tfplan.json
Policy sederhana bisa memblokir delete.
package terraform.deny
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] == "delete"
msg := sprintf("Destroy action requires manual approval: %s", [rc.address])
}
Untuk pipeline lebih lengkap, bacaClaude Code CI/CD danSecrets management.
Kesalahan umum dan hasil percobaan
Kesalahan pertama adalah memperlakukan state seperti file sementara. Jangan commit ke Git, jangan pakai backend key yang sama untuk dev dan prod, dan batasi permission. Kesalahan kedua adalah memakai count.index untuk resource jangka panjang; perubahan urutan list bisa memicu replace. Kesalahan ketiga adalah menyimpan password, token, atau access key di tfvars. Kesalahan keempat adalah apply plan hanya karena Claude Code memberi ringkasan yang terlihat meyakinkan.
Dalam repository test artikel ini, terraform fmt -recursive dan terraform validate berhasil setelah VPC dipisah menjadi module. Untuk plan nyata, bucket backend harus diganti dan AWS credentials harus siap. Risiko yang paling mudah terlewat adalah biaya NAT Gateway, pemisahan state key, dan replace action. Instruksi terbaik untuk Claude Code adalah meminta destroy dan replace diperiksa sebelum komentar gaya penulisan.
Jika tim kamu ingin membuat prompt review, checklist CI, dan materi onboarding IaC yang konsisten, lihatClaude Code resources atauimplementation training. Untuk proyek pribadi pun, menyimpan prompt dan command ini di CLAUDE.md membuat PR Terraform berikutnya lebih aman dan mudah diulang.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.