Claude Code로 Terraform IaC 구현하기: AWS module, state, CI 실전
AWS 예제로 Claude Code와 Terraform IaC를 안전하게 구현하는 방법을 module, state, CI, plan 리뷰까지 정리합니다.
Terraform으로 AWS 인프라를 만들 때 처음에는 VPC와 subnet을 작성하는 것만으로도 충분해 보입니다. 하지만 운영 환경에 가까워질수록 더 중요한 문제가 드러납니다. state를 어디에 보관할지, module을 얼마나 작게 나눌지, dev와 prod를 어떻게 분리할지, CI에서 terraform plan을 누가 검토할지, Claude Code가 만든 변경이 리소스를 삭제하지는 않는지 확인해야 합니다.
IaC는 Infrastructure as Code, 즉 인프라를 코드로 관리하는 방식입니다. HCL은 Terraform 설정 언어이고, module은 재사용 가능한 인프라 부품입니다. state는 Terraform이 실제 클라우드 리소스와 코드의 대응 관계를 기록한 장부이며, backend는 그 state를 저장하는 위치입니다. 이 개념을 먼저 잡아야 Claude Code가 만든 HCL을 안전하게 판단할 수 있습니다.
Masa가 작은 AWS 검증 저장소에서 확인한 결과, Claude Code는 VPC module 분리, variables 정리, CI 초안 작성에 꽤 효과적이었습니다. 다만 “tfvars에 secrets를 쓰지 말 것”, “destroy가 있으면 승인하지 말 것”, “오래된 backend 잠금 방식을 새 프로젝트에 쓰지 말 것” 같은 규칙을 명시하지 않으면 위험한 기본값이 섞일 수 있습니다.
전체 흐름과 사용 사례
안전한 흐름은 요구사항 작성, HCL 생성, Terraform 검증, plan 리뷰, 승인 후 apply입니다.
flowchart LR
A["요구사항을 명확히 작성"] --> B["Claude Code가 HCL 초안 작성"]
B --> C["terraform fmt / validate"]
C --> D["terraform plan"]
D --> E["Claude Code와 사람이 리뷰"]
E --> F["승인 후 apply"]
실무에서 자주 쓰는 사례는 다음과 같습니다.
| 사용 사례 | Claude Code가 도울 수 있는 일 | 사람이 반드시 볼 것 |
|---|---|---|
| 새 AWS 네트워크 | VPC, subnet, route table, tag 초안 | CIDR 충돌, NAT Gateway 비용 |
| module 설계 | modules/vpc 분리, variables와 outputs 작성 | module 책임이 너무 넓지 않은지 |
| 환경 분리 | envs/dev.tfvars, envs/prod.tfvars 구성 | backend key 혼용, prod 권한 |
| CI와 policy | fmt, validate, plan, JSON policy gate | destroy, replace, IAM 권한 확대 |
Claude Code 기본 사용법은공식 문서, Terraform module 개념은Terraform Modules 문서를 참고하세요. AWS 권한 설계가 걱정된다면 내부 글인AWS IAM 가이드도 함께 보면 좋습니다.
Claude Code 프롬프트는 안전 제약부터 적기
Terraform에서는 “AWS용 Terraform 작성해 줘”만으로는 부족합니다. 작업 범위, 금지 사항, 검증 명령을 같이 전달해야 합니다.
claude -p "
AWS VPC module용 Terraform을 작성해 주세요.
제가 명시적으로 요청하지 않는 한 기존 파일은 삭제하지 마세요.
요구사항:
- Terraform 1.10 이상을 가정
- hashicorp/aws provider 사용
- VPC 1개, public subnet 2개, private subnet 2개, Internet Gateway, NAT Gateway 생성
- 재사용 코드는 modules/vpc에 배치
- dev/prod는 tfvars로 분리
- secrets 또는 AWS access key를 tfvars에 쓰지 않기
- terraform fmt -recursive, terraform validate, terraform plan 절차 포함
- plan에 destroy 또는 replacement가 있으면 apply 불가로 판단
"
핵심은 삭제 금지, secrets 금지, destroy 미승인입니다. Claude Code는 빈칸을 일반적인 패턴으로 채웁니다. 애플리케이션 코드에서는 이것이 생산성을 높일 수 있지만, Terraform에서는 비용 증가, 네트워크 공개, 권한 과다 부여로 이어질 수 있습니다.
복사해서 시작할 수 있는 AWS Terraform 구성
아래 구성은 작은 서비스 네트워크를 만들기 위한 출발점입니다. 실제 plan을 보려면 AWS 인증과 state bucket이 필요합니다. 2026년 6월 기준 Terraform S3 backend 문서는 use_lockfile을 통한 S3 잠금을 지원합니다. 기존 DynamoDB 기반 잠금은 deprecated로 표시되어 있으므로 새 프로젝트에서는 공식 문서를 확인하고 사용하지 않는 편이 낫습니다. 자세한 내용은Terraform S3 backend 문서를 보세요.
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-northeast-2"
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-northeast-2"
}
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
}
module 설계와 variables
VPC module은 네트워크만 책임지게 둡니다. ALB, ECS, RDS, IAM까지 한 module에 넣으면 작은 수정도 거대한 plan으로 보입니다. Claude Code에는 “network module은 network만”이라고 반복해서 알려주는 편이 안전합니다.
# 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, 환경 분리
state에는 리소스 ID와 일부 속성이 들어갑니다. 따라서 S3 bucket versioning을 켜고, 환경별 backend key를 분리하며, state 접근 권한을 좁혀야 합니다. dev와 prod가 같은 key를 쓰는 것은 매우 흔한 실수입니다.
# envs/dev.tfvars
aws_region = "ap-northeast-2"
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-northeast-2a" },
{ name = "c", cidr = "10.40.1.0/24", az = "ap-northeast-2c" }
]
private_subnets = [
{ name = "a", cidr = "10.40.10.0/24", az = "ap-northeast-2a" },
{ name = "c", cidr = "10.40.11.0/24", az = "ap-northeast-2c" }
]
production에서는 CIDR, tag, backend key를 모두 바꿉니다. workspace도 가능하지만, 초보 팀에는 envs/*.tfvars와 명시적인 backend key가 더 읽기 쉽습니다.
fmt, validate, plan 검증 절차
Terraform 공식 문서는 fmt, validate, plan을 각각 설명합니다. 먼저 backend 없이 구문과 타입을 확인합니다.
cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate
실제 AWS 변경을 보려면 backend와 인증을 준비한 뒤 plan을 저장합니다.
terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt
Claude Code에는 plan 요약보다 위험 탐지를 요청합니다.
claude -p "
plan.txt를 검토해 주세요.
destroy, replace, force replacement, IAM 권한 확대, public subnet 공개, backend key 변경을 우선적으로 지적하세요.
삭제가 포함되어 있으면 승인하지 마세요.
사람 reviewer가 답해야 할 질문을 짧게 정리하세요.
"
CI, policy, 권한과 secrets
GitHub Actions에는 장기 AWS access key를 저장하지 않는 것이 좋습니다. OIDC로 좁은 IAM role을 assume하는 구성이 기본이며, 공식 설명은GitHub Actions OIDC in AWS에 있습니다. IAM 정책은AWS 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-northeast-2
- 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
정책 검사를 넣으면 명백히 위험한 plan을 자동으로 멈출 수 있습니다.
package terraform.deny
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] == "delete"
msg := sprintf("Destroy action requires manual approval: %s", [rc.address])
}
CI 전체는Claude Code CI/CD 가이드, secrets 관리는Claude Code Secrets 관리도 참고하세요.
자주 실패하는 지점과 실제 결과
첫째, state를 로컬 파일처럼 다루면 안 됩니다. Git에 올리거나 dev/prod가 같은 key를 쓰면 팀 작업이 바로 위험해집니다. 둘째, count.index로 오래 유지될 리소스를 관리하면 리스트 순서 변경만으로 replace가 날 수 있습니다. 셋째, tfvars에 비밀번호나 API token을 넣는 실수입니다. sensitive = true는 화면 출력을 줄일 뿐 state 자체를 안전하게 만들지는 않습니다. 넷째, Claude Code가 plan을 요약했다고 바로 apply하는 실수입니다. NAT Gateway, RDS, EKS, IAM 변경은 비용과 장애 영향이 크기 때문에 사람이 확인해야 합니다.
이 글의 예제를 검증 저장소에 넣었을 때 terraform fmt -recursive와 terraform validate는 통과했습니다. 실제 plan에서는 backend bucket 이름을 바꾸지 않으면 초기화가 멈추고, NAT Gateway 비용과 state key 분리가 쉽게 놓친다는 점도 확인했습니다. Claude Code에는 스타일보다 destroy와 replace를 먼저 보라고 지시했을 때 리뷰 품질이 가장 안정적이었습니다.
팀에서 IaC 리뷰 규칙과 교육 자료를 정리하려면Claude Code 자료 모음이나도입 상담을 활용할 수 있습니다. 개인 프로젝트라도 이 글의 프롬프트와 검증 명령을 CLAUDE.md에 넣어 두면 다음 Terraform PR의 품질이 훨씬 일정해집니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.