Terraform IaC mit Claude Code: AWS-Module, State, CI und Plan-Review
Praxisguide für Terraform IaC mit Claude Code auf AWS: Module, Backend, State, CI, Policy, Secrets und typische Fehler.
Terraform ist schnell gestartet, aber nicht automatisch sicher. Ein generiertes VPC-Setup kann syntaktisch korrekt sein und trotzdem den State lokal ablegen, dev und prod auf denselben Backend-Key zeigen lassen oder im terraform plan eine Ersetzung auslösen, die niemand bemerkt. Claude Code hilft besonders dann, wenn du es als Implementierungs- und Review-Assistenten nutzt, nicht als automatischen Apply-Knopf.
IaC steht für Infrastructure as Code: Cloud-Ressourcen werden in versionierten Dateien beschrieben. HCL ist die Konfigurationssprache von Terraform. Ein Module ist ein wiederverwendbarer Infrastrukturbaustein. State ist das Register, mit dem Terraform Code und reale Ressourcen verbindet. Das Backend speichert diesen State. Wer diese Begriffe sauber trennt, kann Claude-Code-Vorschläge deutlich besser bewerten.
Für diesen Artikel hat Masa ein kleines AWS-Test-Repository verwendet. Claude Code war stark beim Aufteilen eines VPC in ein Module, beim Ergänzen von Variable-Validierungen und beim Entwurf einer CI-Pipeline. Ohne klare Grenzen kamen aber riskante Defaults vor: secrets in tfvars, kein harter Stopp bei destroy und alte Beispiele mit DynamoDB-Locking für S3 Backends.
Workflow und typische Einsatzfälle
Der sichere Ablauf lautet: Anforderungen formulieren, HCL generieren, Terraform prüfen, Plan reviewen, erst dann apply.
flowchart LR
A["Infra-Brief schreiben"] --> B["Claude Code entwirft HCL"]
B --> C["terraform fmt / validate"]
C --> D["terraform plan"]
D --> E["KI- und Menschenreview"]
E --> F["Freigegebener apply"]
In der Praxis taucht dieses Muster in mehreren Situationen auf.
| Einsatzfall | Was Claude Code vorbereiten kann | Was ein Mensch prüfen muss |
|---|---|---|
| Neues AWS-Netzwerk | VPC, Subnets, Routes, Tags | CIDR-Kollisionen, NAT-Gateway-Kosten |
| Module-Struktur | modules/vpc, variables, outputs | Ob das Module zu viel Verantwortung trägt |
| Umgebungstrennung | envs/dev.tfvars, envs/prod.tfvars | Backend-Key, Produktionsrechte |
| CI und Policy | fmt, validate, plan, JSON-Checks | destroy, replace, IAM-Erweiterung |
Die Produktgrundlagen stehen in derClaude Code Dokumentation. Für Module ist dieTerraform Modules Dokumentation maßgeblich. Für Berechtigungen passt der interneAWS-IAM-Guide als Ergänzung.
Claude Code mit klaren Grenzen prompten
Ein Terraform-Prompt muss Ressourcen, Verbote und Prüfschritte enthalten. Sonst füllt Claude Code Lücken mit allgemeinen Mustern.
claude -p "
Erstelle Terraform nur für ein AWS VPC Module.
Lösche keine bestehenden Dateien, außer ich fordere es ausdrücklich an.
Anforderungen:
- Terraform 1.10 oder neuer
- Provider hashicorp/aws
- VPC, 2 public subnets, 2 private subnets, Internet Gateway, NAT Gateway
- Wiederverwendbarer Code unter modules/vpc
- dev und prod über tfvars trennen
- Keine secrets oder AWS access keys in tfvars schreiben
- terraform fmt -recursive, terraform validate und terraform plan dokumentieren
- Wenn der plan destroy oder replacement enthält, gilt er als nicht freigegeben
"
Die wichtigsten Grenzen sind: kein Löschen, keine secrets, kein automatisches Freigeben von destroy. Gerade bei Terraform können generische Defaults Kosten verursachen, Netzwerke öffnen oder Berechtigungen zu breit setzen.
Kopierbares AWS-Terraform-Grundgerüst
Die folgende Struktur ist klein genug für den Einstieg. Mit AWS-Credentials und einem echten State-Bucket kannst du sie formatieren, validieren und planen. Im Juni 2026 unterstützt das Terraform S3 Backend Locking über use_lockfile; DynamoDB-basiertes Locking ist in der offiziellen Dokumentation als deprecated markiert. Prüfe daher das offizielleS3 Backend, bevor du ältere Beispiele kopierst.
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 = "eu-central-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 = "eu-central-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-Design und Variables
Das VPC Module sollte Netzwerk bleiben. Wenn ALB, ECS, RDS und IAM im selben Module landen, wird jeder Plan schwer lesbar. Verwende außerdem stabile Namen mit for_each, statt langlebige Ressourcen nur über count.index zu adressieren.
# 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 und Umgebungstrennung
State ist kein harmloser Cache. Er enthält Ressourcen-IDs und teils sensible Attribute. Nutze S3 Versioning, Locking, enge IAM-Rechte und getrennte Backend-Keys pro Umgebung.
# envs/dev.tfvars
aws_region = "eu-central-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 = "eu-central-1a" },
{ name = "b", cidr = "10.40.1.0/24", az = "eu-central-1b" }
]
private_subnets = [
{ name = "a", cidr = "10.40.10.0/24", az = "eu-central-1a" },
{ name = "b", cidr = "10.40.11.0/24", az = "eu-central-1b" }
]
Für production müssen CIDR, Tags und Backend-Key angepasst werden. Workspaces sind möglich, aber explizite tfvars Dateien sind für Einsteiger leichter zu reviewen.
Prüfung mit fmt, validate und plan
Die offiziellen Terraform-Seiten erklären fmt, validate und plan. Starte mit einer Prüfung ohne Remote-Backend.
cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate
Danach initialisierst du das echte Backend und speicherst den Plan.
terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt
Für Claude Code sollte die Review-Anfrage risikoorientiert sein.
claude -p "
Review plan.txt.
Priorisiere destroy, replace, force replacement, IAM-Erweiterung, öffentliche Exposition und Backend-Key-Änderungen.
Gib keine Freigabe, wenn Löschungen enthalten sind.
Liste konkrete Fragen für den menschlichen Reviewer.
"
CI, Policy, Rechte und Secrets
In GitHub Actions sollten keine langlebigen AWS Access Keys liegen. Nutze OIDC, um eine eng begrenzte IAM Role zu übernehmen. Die offizielle Anleitung istGitHub Actions OIDC in AWS. Die Rolle sollte denAWS IAM best practices folgen.
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: eu-central-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
Eine einfache Policy kann destruktive Pläne blockieren.
package terraform.deny
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] == "delete"
msg := sprintf("Destroy action requires manual approval: %s", [rc.address])
}
Mehr zum Pipeline-Design steht imClaude Code CI/CD Guide. Für sensitive Werte passt derSecrets-Management-Guide.
Typische Fehler und Testergebnis
Der erste Fehler ist ein unsicherer State: lokal, in Git oder mit einem gemeinsamen Key für dev und prod. Der zweite ist count.index für langlebige Ressourcen; eine geänderte Reihenfolge kann replacements erzeugen. Der dritte ist das Speichern von Passwörtern oder Tokens in tfvars. sensitive = true reduziert Ausgabe, macht den State aber nicht automatisch sicher. Der vierte Fehler ist, einen Plan anzuwenden, nur weil Claude Code ihn ordentlich zusammenfasst.
Im Test-Repository liefen terraform fmt -recursive und terraform validate nach der Module-Trennung sauber durch. Für einen echten Plan müssen Backend-Bucket und AWS-Credentials ersetzt werden. Auffällig waren NAT-Gateway-Kosten, Backend-Key-Trennung und implizite replacements. Die beste Claude-Code-Anweisung war, destroy und replace vor Stilfragen zu priorisieren.
Wenn dein Team Review-Prompts, CI-Checklisten und IaC-Onboarding standardisieren will, nutze dieClaude Code Bibliothek oder dasImplementierungstraining. Auch in Einzelprojekten lohnt es sich, Prompt, Befehle und Review-Regeln in CLAUDE.md abzulegen.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.