Use Cases (更新: 2026/6/2)

用 Claude Code 实现 Terraform IaC:AWS 模块、状态与 CI 实战

以AWS为例讲解Claude Code如何辅助Terraform IaC,覆盖module、state、CI、plan审查和常见坑。

用 Claude Code 实现 Terraform IaC:AWS 模块、状态与 CI 实战

用 Terraform 写 AWS 基础设施时,真正难的往往不是把第一个 VPC 写出来,而是后面的治理:state 放在哪里、module 怎么拆、dev 和 prod 如何隔离、CI 怎样审查 terraform plan、Claude Code 生成的代码是否会误删资源。IaC 是 Infrastructure as Code,也就是把云资源配置写成可以审查和版本管理的代码。HCL 是 Terraform 的配置语言,module 是可复用的基础设施组件,state 是 Terraform 记录真实资源的账本,backend 是保存 state 的位置。

Claude Code 在 Terraform 场景里很有用,但它不应该被当成“自动上线按钮”。更安全的用法是:让它根据清晰的约束生成 HCL,再让它帮助审查 plan、指出破坏性变更和权限扩大。Masa 在一个小型 AWS 验证仓库里试过,Claude Code 对 module 拆分、variables 校验、CI 草稿非常高效;但如果不明确禁止 secrets 写入 tfvars、不要求 destroy 人工确认,它也可能给出过于宽泛的默认方案。

工作流与适用场景

推荐的顺序是先写需求,再生成代码,然后执行 Terraform 检查,最后审查 plan。

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、tagCIDR 是否冲突,NAT Gateway 成本
module 化拆成 modules/vpc、整理 variables 和 outputsmodule 是否承担太多职责
环境隔离生成 envs/dev.tfvarsenvs/prod.tfvarsbackend key 是否混用,prod 权限是否过大
CI 和 policy生成 fmt、validate、plan、policy checkdestroy、replace、IAM 扩权

Claude Code 的基础用法可看官方文档,Terraform module 概念可看Terraform Modules 官方文档。如果你要先补 AWS 权限设计,可以继续读站内的AWS IAM 指南

给 Claude Code 的提示词要带约束

不要只说“帮我写 Terraform”。要把可触碰范围、禁止事项和验证命令写进去。

claude -p "
请为 AWS 创建 Terraform 配置,只处理 VPC module。
不要删除现有文件,除非我明确要求。

要求:
- 假设 Terraform 1.10 或更高版本
- 使用 hashicorp/aws provider
- 创建 VPC、2 个 public subnet、2 个 private subnet、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
"

这段提示词的价值在于明确了安全边界。Terraform 不是普通脚手架,生成出来的代码会花钱、改网络、影响权限。Claude Code 可以帮你写得更快,但成本、停机和安全风险仍然要由人来审查。

可复制的 AWS Terraform 结构

下面的结构可以作为小型项目起点。真实执行前需要 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-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-northeast-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
}

module 设计与 variables

VPC module 只负责网络。不要把 ALB、ECS、RDS、IAM 全塞进去,否则 plan 会很大,审查也会变得模糊。下面的写法用 for_each 和具名 subnet,避免单纯依赖 count.index 造成顺序变化引发 replace。

# 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
}
# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = [for subnet in aws_subnet.private : subnet.id]
}

state、backend 与环境隔离

state 不是普通缓存。它保存资源 ID 和部分属性,如果权限过宽,就等于把基础设施账本交给了太多人。S3 bucket 应开启 versioning,IAM 只允许需要的人访问对应环境的 state。dev 和 prod 不要共享同一个 key

# envs/dev.tfvars
aws_region  = "ap-northeast-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-northeast-1a" },
  { name = "c", cidr = "10.40.1.0/24", az = "ap-northeast-1c" }
]

private_subnets = [
  { name = "a", cidr = "10.40.10.0/24", az = "ap-northeast-1a" },
  { name = "c", cidr = "10.40.11.0/24", az = "ap-northeast-1c" }
]

如果是 production,CIDR、tag、backend key 都要改。workspace 对成熟团队有帮助,但对初学者来说,明确的 envs/*.tfvars 加单独 backend key 更容易审查。

fmt、validate、plan 的验证步骤

Terraform 官方说明了 fmtvalidateplan 的用途。先做不连接远端 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,官方说明见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-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 可以把明显危险的 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 或放在个人电脑上。第二个失败是 count.index 用在长期资源上,列表顺序一变就可能 replace。第三个失败是把 DB 密码、API token、AWS key 写入 tfvars;即使变量标了 sensitive,state 里仍可能留下值。第四个失败是看到 Claude Code 生成了 plan 总结,就直接 apply。NAT Gateway、RDS、EKS、IAM 这类资源必须人工确认成本、停机和权限影响。

本文的示例在小型验证仓库中运行后,terraform fmt -recursiveterraform validate 可以通过。真实 plan 前必须替换 backend bucket,并准备 AWS 凭证。实际审查时最容易被忽略的是 NAT Gateway 成本、dev/prod state key 混用、以及 plan 中的 replace。把这些风险写进 Claude Code 提示词后,审查结果明显更稳定。

如果你的团队想把 Terraform 审查提示词、CI 模板和新人上手材料整理成标准流程,可以参考Claude Code 教材导入咨询。个人项目也建议把本文的提示词和检查命令写进 CLAUDE.md,让每次 IaC 修改都从同一套安全规则开始。

#Claude Code #Terraform #IaC #AWS #infrastructure
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。