Use Cases (Mis à jour: 02/06/2026)

Terraform IaC avec Claude Code : modules AWS, state, CI et revue de plan

Guide pratique pour utiliser Claude Code avec Terraform IaC sur AWS : modules, backend, state, CI, policy et pièges.

Terraform IaC avec Claude Code : modules AWS, state, CI et revue de plan

Terraform permet de gérer l’infrastructure comme du code, mais un fichier HCL qui fonctionne n’est pas forcément prêt pour la production. Un VPC généré peut être correct tout en stockant le state localement, en partageant la même clé backend entre dev et prod, ou en préparant un remplacement de subnet que personne ne remarque dans le terraform plan. Claude Code devient utile quand on l’utilise comme assistant de conception et de revue, pas comme bouton d’exécution automatique.

IaC signifie Infrastructure as Code : décrire les ressources cloud dans des fichiers versionnés et relus. HCL est le langage de configuration de Terraform. Un module est un composant réutilisable. Le state est le registre qui relie le code Terraform aux ressources réellement créées. Le backend est l’endroit où ce state est stocké. Ces notions sont simples, mais la plupart des incidents Terraform apparaissent justement entre code, state, droits et revue humaine.

Pour cet article, Masa a testé un petit dépôt AWS. Claude Code a bien aidé à séparer un VPC en module, à écrire des variables validées et à produire une base de CI. Mais les garde-fous devaient être explicites : pas de secrets dans les tfvars, pas d’approbation automatique si le plan contient un destroy, et pas de vieux modèle de verrouillage S3 basé sur DynamoDB pour un nouveau projet.

Flux de travail et cas d’usage

Le flux sûr est le suivant : brief clair, génération HCL, vérification Terraform, revue du plan, puis apply après approbation.

flowchart LR
  A["Rédiger le brief infra"] --> B["Claude Code propose le HCL"]
  B --> C["terraform fmt / validate"]
  C --> D["terraform plan"]
  D --> E["Revue IA et humaine"]
  E --> F["Apply approuvé"]

Les cas d’usage les plus utiles sont récurrents.

Cas d’usageCe que Claude Code prépareCe que l’humain vérifie
Nouveau réseau AWSVPC, subnets, routes, tagsCIDR, coût NAT Gateway
Module reusablemodules/vpc, variables, outputsResponsabilité du module
Séparation dev/prodenvs/dev.tfvars, envs/prod.tfvarsClé backend, droits prod
CI et policyfmt, validate, plan, checks JSONdestroy, replace, IAM trop large

Pour le produit, consultez ladocumentation officielle Claude Code. Pour les modules, la référence est ladocumentation Terraform Modules. Côté permissions, le guide interneAWS IAM complète bien cet article.

Donner des contraintes à Claude Code

Un prompt Terraform doit décrire les ressources, mais aussi les interdits et la vérification attendue.

claude -p "
Crée une configuration Terraform pour un module VPC AWS uniquement.
Ne supprime aucun fichier existant sans demande explicite.

Exigences:
- Terraform 1.10 ou plus récent
- Provider hashicorp/aws
- VPC, 2 subnets publics, 2 subnets privés, Internet Gateway, NAT Gateway
- Code réutilisable dans modules/vpc
- Séparer dev et prod avec des fichiers tfvars
- Ne pas écrire de secrets ni d'AWS access keys dans tfvars
- Inclure terraform fmt -recursive, terraform validate et terraform plan
- Si le plan contient destroy ou replacement, il n'est pas approuvé
"

Les mots importants sont “uniquement”, “ne supprime”, “pas de secrets” et “pas approuvé”. Sans ces limites, Claude Code complète les zones floues avec des patterns génériques. En infrastructure, un pattern générique peut coûter cher ou exposer un réseau.

Squelette Terraform AWS prêt à adapter

Cette structure est volontairement courte. Avec des identifiants AWS et un bucket de state réel, elle peut être formatée, validée et planifiée. En juin 2026, le backend S3 Terraform prend en charge le verrouillage avec use_lockfile. Le verrouillage DynamoDB est indiqué comme deprecated dans la documentation officielle. Vérifiez lebackend S3 officiel avant de copier un ancien tutoriel.

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-west-3"
    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-west-3"
}

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
}

Concevoir le module et les variables

Le module VPC doit rester un module réseau. Si vous y ajoutez ALB, ECS, RDS et IAM, une petite modification devient un plan illisible. La version ci-dessous utilise for_each avec des noms stables afin d’éviter les remplacements causés par un simple changement d’ordre dans une liste.

# 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 et séparation des environnements

Le state peut contenir des identifiants et des attributs sensibles. Utilisez un bucket S3 avec versioning, un verrouillage, des permissions strictes et une clé différente par environnement.

# envs/dev.tfvars
aws_region  = "eu-west-3"
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-west-3a" },
  { name = "b", cidr = "10.40.1.0/24", az = "eu-west-3b" }
]

private_subnets = [
  { name = "a", cidr = "10.40.10.0/24", az = "eu-west-3a" },
  { name = "b", cidr = "10.40.11.0/24", az = "eu-west-3b" }
]

Pour prod, changez le CIDR, les tags et la clé backend. Les workspaces sont possibles, mais des fichiers tfvars explicites restent plus lisibles pour une équipe qui débute.

Vérifier avec fmt, validate et plan

Les commandes officielles sont documentées ici : fmt, validate et plan. Commencez sans backend distant.

cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate

Puis initialisez le backend réel et sauvegardez le plan.

terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt

Demandez une revue orientée risques.

claude -p "
Relis plan.txt.
Priorité à destroy, replace, force replacement, élargissement IAM, exposition publique et changement de backend key.
N'approuve pas le plan s'il contient une suppression.
Liste les questions à poser au reviewer humain.
"

CI, policy, permissions et secrets

Dans GitHub Actions, évitez les access keys AWS de longue durée. Utilisez OIDC pour assumer un rôle IAM limité, comme expliqué dans le guide officielGitHub Actions OIDC in AWS. Les permissions doivent suivre lesAWS 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: eu-west-3
      - 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

Une règle policy minimale peut bloquer les suppressions.

package terraform.deny

deny[msg] {
  rc := input.resource_changes[_]
  rc.change.actions[_] == "delete"
  msg := sprintf("Destroy action requires manual approval: %s", [rc.address])
}

Pour aller plus loin, voyez le guideCI/CD Claude Code et le guidesecrets management.

Pièges fréquents et résultat du test

Le premier piège consiste à traiter le state comme un fichier temporaire. Ne le commitez pas, ne partagez pas la même clé entre dev et prod, et limitez les droits d’écriture. Le deuxième piège est l’usage de count.index pour des ressources durables : un simple réordonnancement peut déclencher un remplacement. Le troisième est de placer des mots de passe ou tokens dans les tfvars. Le quatrième est d’appliquer un plan parce que Claude Code l’a résumé de façon convaincante.

Dans le dépôt de test, terraform fmt -recursive et terraform validate sont passés après la séparation en module. Le plan réel exige de remplacer le bucket backend et de configurer les identifiants AWS. Les points les plus faciles à manquer étaient le coût du NAT Gateway, la séparation des clés de state et les remplacements implicites. La meilleure consigne à Claude Code a été de chercher destroy et replace avant tout commentaire de style.

Si votre équipe veut standardiser les prompts de revue, les checklists CI et l’onboarding IaC, consultez labibliothèque Claude Code ou laformation d’implémentation. Même en solo, placer ces consignes dans CLAUDE.md rend les prochaines pull requests Terraform plus prévisibles.

#Claude Code #Terraform #IaC #AWS #infrastructure
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.