Use Cases (Aktualisiert: 2.6.2026)

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 IaC mit Claude Code: AWS-Module, State, CI und Plan-Review

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.

EinsatzfallWas Claude Code vorbereiten kannWas ein Mensch prüfen muss
Neues AWS-NetzwerkVPC, Subnets, Routes, TagsCIDR-Kollisionen, NAT-Gateway-Kosten
Module-Strukturmodules/vpc, variables, outputsOb das Module zu viel Verantwortung trägt
Umgebungstrennungenvs/dev.tfvars, envs/prod.tfvarsBackend-Key, Produktionsrechte
CI und Policyfmt, validate, plan, JSON-Checksdestroy, 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.

#Claude Code #Terraform #IaC #AWS #infrastructure
Kostenlos

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.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.