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 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’usage | Ce que Claude Code prépare | Ce que l’humain vérifie |
|---|---|---|
| Nouveau réseau AWS | VPC, subnets, routes, tags | CIDR, coût NAT Gateway |
| Module reusable | modules/vpc, variables, outputs | Responsabilité du module |
| Séparation dev/prod | envs/dev.tfvars, envs/prod.tfvars | Clé backend, droits prod |
| CI et policy | fmt, validate, plan, checks JSON | destroy, 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.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.