Claude CodeでTerraform IaCを実装するAWS実践ガイド【module/state/CI】
Claude CodeでTerraform IaCを安全に実装する手順をAWS例で解説。module設計、state、CI、planレビューまで網羅。
TerraformでAWSインフラを作ると、最初は「VPCを書けた」で満足しがちです。けれど本番に近づくほど、stateの置き場所、moduleの粒度、stagingとproductionの分離、IAM権限、CIでのplanレビューが問題になります。ここを曖昧にしたままClaude Codeへ「Terraformを書いて」と頼むと、動くけれど危ないコードが混ざります。
この記事ではClaude Codeを、Terraformを書くAIではなく「IaCの実装を一緒に点検する相棒」として使います。IaCはInfrastructure as Code、つまりインフラ構成をコードで管理する考え方です。HCLはTerraformの設定言語、moduleは再利用できる部品、stateは実環境とコードの対応表、backendはstateの保存先です。初出の用語はこのように日本語へ置き換えながら進めます。
Masaが小さなAWS検証環境で試した範囲では、Claude Codeはmodule分割や変数の整理をかなり速くしてくれます。一方で「削除を含むplanをそのままapplyしない」「secretsをtfvarsへ置かない」「S3 backendのロック方式を古いままにしない」という人間側のレビュー指示がないと、危ない提案も出ます。
全体像とユースケース
この記事の流れは、要件をClaude Codeへ渡し、HCLを生成し、Terraform CLIとCIで検証し、破壊的変更を人間が止める、という順番です。
flowchart LR
A["要件を日本語で渡す"] --> B["Claude CodeがHCLを下書き"]
B --> C["terraform fmt / validate"]
C --> D["terraform plan"]
D --> E["Claude Codeと人間で差分レビュー"]
E --> F["承認後にapply"]
実務で使いやすいユースケースは少なくとも次の4つです。
| ユースケース | Claude Codeに任せること | 人間が必ず見ること |
|---|---|---|
| 新規サービスのVPC作成 | VPC、subnet、route tableのHCL下書き | CIDR衝突、NAT Gatewayのコスト |
| module化 | modules/vpcへの分割、variablesとoutputsの整理 | moduleが責務を抱えすぎていないか |
| 環境分離 | envs/dev.tfvarsとenvs/prod.tfvarsの差分設計 | productionだけ広い権限になっていないか |
| CI policy | fmt、validate、plan、policy checkのワークフロー | destroyやreplaceが混ざったplan |
Claude Codeの基本はClaude Code公式ドキュメントを、Terraformのmodule設計はTerraform Modules公式ドキュメントを確認してください。AWS権限の考え方はサイト内のAWS IAMガイドも合わせて読むとつながります。
Claude Codeへの依頼を安全にする
最初のプロンプトは、短くても制約を明確にします。Claude Codeには「コードだけ」ではなく、触ってよい範囲、禁止事項、検証コマンドまで渡します。
claude -p "
AWS向けTerraformを作成してください。
対象はVPC moduleだけです。既存ファイルを勝手に削除しないでください。
要件:
- Terraform 1.10以降を想定
- AWS providerはhashicorp/awsを使う
- VPC、public subnet 2つ、private subnet 2つ、Internet Gateway、NAT Gatewayを作る
- moduleはmodules/vpcに分ける
- dev/prodはtfvarsで分離する
- secretsやAWS access keyをtfvarsに書かない
- terraform fmt -recursive、terraform validate、terraform planの手順も出す
- planにdestroyまたはreplaceが出る場合はapplyしない前提でレビュー観点を書く
"
ポイントは「削除しない」「secretsを書かない」「destroyを止める」を明文化することです。Claude Codeは依頼が曖昧なほど一般論で補います。Terraformではその一般論が、広すぎるIAMや共有state keyのような事故につながります。
コピペで始めるTerraform構成
次の構成は、AWS認証とbackend用S3 bucketを用意すれば実際に検証できます。S3 backendは2026年6月時点の公式ドキュメントでuse_lockfileによるロックに対応しています。従来のDynamoDBロックはdeprecated扱いなので、新規構成では避けます。詳細はTerraform S3 backend公式ドキュメントを参照してください。
infra/
versions.tf
variables.tf
main.tf
outputs.tf
envs/dev.tfvars
modules/vpc/main.tf
modules/vpc/variables.tf
modules/vpc/outputs.tf
まずroot moduleです。
# 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
}
# outputs.tf
output "vpc_id" {
value = module.network.vpc_id
description = "Created VPC ID."
}
output "private_subnet_ids" {
value = module.network.private_subnet_ids
description = "Private subnet IDs for app workloads."
}
module設計とvariables
moduleは「AWSネットワークだけ」を担当させます。ALB、ECS、RDSまで同じmoduleに入れると、少し変えるだけで巨大なplanになります。Claude Codeへは「責務を広げない」と伝えます。
# 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
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-igw"
})
}
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
}
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-public-rt"
})
}
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"
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-nat-eip"
})
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = values(aws_subnet.public)[0].id
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-nat"
})
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
}
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-private-rt"
})
}
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 "public_subnet_ids" {
value = [for subnet in aws_subnet.public : subnet.id]
}
output "private_subnet_ids" {
value = [for subnet in aws_subnet.private : subnet.id]
}
backend、state、環境分離
stateにはリソースIDや一部の属性が入ります。権限を緩くすると、インフラの設計図を外に出すのと同じです。S3 bucketはversioningを有効にし、backendのkeyは環境ごとに分けます。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の分離が読みやすいです。Claude Codeには「productionのbackend keyをdevと共有しない」「prod tfvarsには本番用CIDRを明記する」と指示してください。
fmt、validate、planの検証手順
Terraform CLIの公式コマンドはfmt、validate、planを確認してください。ローカルではまず構文と型を検証します。
cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate
実際のAWS差分を見るときは、backend bucketとAWS認証を準備してからplanを実行します。
terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt
applyはplanをレビューした後です。
terraform apply tfplan
Claude Codeへplanをレビューさせるときは、次のように破壊的変更を明示的に止めます。
claude -p "
plan.txtをレビューしてください。
destroy、replace、force replacement、IAM権限拡大、public subnet公開、state key変更を最優先で指摘してください。
削除を含む場合は承認しないでください。
安全に進めるための確認質問を箇条書きで出してください。
"
CIとpolicyで人間の見落としを減らす
GitHub Actionsでは静的なAWS access keyを置かず、OIDCでAWS IAM roleを引き受ける構成が基本です。公式手順はGitHub Actions OIDC for 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 checkを入れるなら、plan JSONに対して「危ない差分」を落とします。たとえばpublic access blockを弱める変更はレビュー必須です。
package terraform.deny
deny[msg] {
rc := input.resource_changes[_]
rc.type == "aws_s3_bucket_public_access_block"
after := rc.change.after
after.block_public_acls == false
msg := sprintf("S3 public ACL blocking is disabled: %s", [rc.address])
}
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管理も参考になります。
失敗しやすい落とし穴
1つ目は、stateを軽く見ることです。terraform.tfstateをローカルやGitに置くと、チーム作業で差分が壊れます。S3 backend、versioning、lockfile、厳しいIAMを最初に整えてください。
2つ目は、count.indexに頼りすぎることです。subnetの順番を変えただけでreplaceが出る場合があります。この記事の例のようにfor_eachと名前付きmapへ寄せると、差分が読みやすくなります。
3つ目は、Claude Codeに「本番へapplyして」と雑に頼むことです。Claude Codeは実行前のレビューを助けますが、費用や停止時間の責任は持ちません。NAT Gateway、RDS、EKSのように課金や停止影響が大きいリソースは、planに出た時点で人間が止まって確認します。
4つ目は、tfvarsへsecretを書くことです。DB password、API token、AWS access keyはSecrets ManagerやCI secretsへ逃がします。Terraformでsecret値を扱う場合もstateに残る可能性を前提にし、sensitive = trueだけで安全になったと誤解しないでください。
5つ目は、moduleを巨大化させることです。VPC moduleにALB、ECS、RDS、IAMまで入れると、再利用よりも修正コストが増えます。Claude Codeには「network moduleはnetworkだけ」と境界を繰り返し伝えます。
まとめと実際に試した結果
Claude CodeでTerraform IaCを実装するコツは、生成よりもレビューの型を作ることです。module設計、variables、backend/state、plan/apply、policy/CI、環境分離、権限とsecretsを最初の依頼に入れると、出てくるHCLの品質が安定します。
この記事で紹介した内容を実際に試した結果、terraform fmt -recursiveとterraform validateはすぐ通りました。terraform planではNAT GatewayとEIPの課金が見落としやすいこと、backend bucket名を仮のままにすると初期化で止まること、prodとdevのstate keyを分け忘れるとレビューで気づきにくいことが確認できました。Claude Codeにはplanの要約だけでなく、destroyとreplaceを最優先で拾う指示を入れるのが有効でした。
チームでIaCレビュー基準を整えたい場合はClaude Code教材一覧や導入相談も活用してください。個人開発でも、この記事のプロンプトとCI雛形をCLAUDE.mdに入れておくと、毎回のTerraform作業がかなり安全になります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。