Use Cases (Diperbarui: 2/6/2026)

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 IaC dengan Claude Code: AWS module, state, CI, dan review plan

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 caseYang bisa dibuat Claude CodeYang harus dicek manusia
Network AWS baruVPC, subnets, routes, tagsCIDR overlap dan biaya NAT Gateway
Desain modulemodules/vpc, variables, outputsBatas tanggung jawab module
Pemisahan environmentenvs/dev.tfvars, envs/prod.tfvarsBackend key dan permission prod
CI dan policyfmt, validate, plan, JSON checksdestroy, 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.

#Claude Code #Terraform #IaC #AWS #infrastructure
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.