Le premier échec sur AWS n'est presque jamais le HCL. C'est la configuration du provider : où appelle-t-il, avec quelle identité, et que valide-t-il avant même de planifier ? Ce guide traite ces trois questions dans l'ordre, puis crée une instance et prouve qu'elle tourne.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comment le provider AWS résout son identité, et pourquoi il échoue avant le plan quand il n'en trouve pas
- Épingler le provider sur une majeure bornée des deux côtés, et vérifier la version réellement installée
- Poser des tags une fois pour toute une racine avec
default_tags, et savoir où ils apparaissent - Lire une AMI sans coder son identifiant, et ce qui protège vraiment contre une image inattendue
- Prouver l'état obtenu par les sorties structurées, pas par un message de succès
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (les exemples sont vérifiés sur 1.15.4)
- Un compte AWS avec des droits suffisants pour créer une instance, ou l'émulateur local du lab associé
- L'AWS CLI configurée si vous visez le vrai AWS, ce que la première section détaille
Où le provider va chercher son identité
Section intitulée « Où le provider va chercher son identité »Avant toute chose, comprenez ce mécanisme : il explique la majorité des échecs du premier jour. Le provider AWS cherche des identifiants dans un ordre déterminé, et s'arrête au premier trouvé.
- Les arguments du bloc
providereux-mêmes (access_key,secret_key,profile) - Les variables d'environnement
AWS_ACCESS_KEY_IDetAWS_SECRET_ACCESS_KEY - Les fichiers partagés
~/.aws/credentialset~/.aws/config, queaws configureécrit - La metadata API de l'instance (IMDS), quand Terraform tourne lui-même sur EC2 ou ECS
Cet ordre a une conséquence pratique importante. Une configuration qui fonctionne chez vous peut échouer en CI, non parce que le HCL diffère, mais parce que le poste portait un ~/.aws que le runner n'a pas.
Quand la chaîne n'aboutit sur rien, le plan échoue avant de planifier quoi que ce soit, avec un message qui nomme les deux dernières étapes tentées :
Error: No valid credential sources found
Error: failed to refresh cached credentials, no EC2 IMDS role found,operation error ec2imds: GetMetadata, access disabled to EC2 IMDS via clientRetenez la forme de ce message : il ne dit pas que votre code est faux, il dit que le provider n'a pas d'identité.
Les manières de fournir cette identité
Section intitulée « Les manières de fournir cette identité »Le bloc provider accepte plusieurs formes, et toutes ne se valent pas.
| Forme | Quand l'utiliser |
|---|---|
profile = "nom" | Poste de travail avec plusieurs comptes, le plus courant en local |
assume_role { role_arn = ... } | Accès inter-comptes, la forme normale en entreprise |
assume_role_with_web_identity | CI moderne par OIDC, sans secret de longue durée |
access_key / secret_key | À réserver aux tests contre un émulateur |
Les clés statiques écrites dans le HCL sont à proscrire face à un vrai compte : elles finissent dans Git. Elles restent en revanche la bonne réponse face à un émulateur local, qui n'authentifie rien mais exige que quelque chose soit fourni.
Épingler le provider, et vérifier ce qui est installé
Section intitulée « Épingler le provider, et vérifier ce qui est installé »Le bloc terraform fixe les contraintes. Créez terraform.tf :
terraform { required_version = ">= 1.11.0"
required_providers { aws = { source = "hashicorp/aws" version = ">= 6.0, < 7.0" } }}Deux choses méritent d'être soulignées ici. La contrainte vise la majeure 6, actuelle depuis 2025, alors que d'innombrables exemples en ligne traînent encore un ~> 5.0 qui vous ferait démarrer sur une majeure précédente. Et elle est bornée des deux côtés : sans plafond, la majeure 7 s'installerait le jour de sa sortie, avec ses ruptures.
Ne vous fiez pas à ce que vous croyez avoir installé. Demandez-le :
terraform initterraform version -json | jq '.provider_selections'{ "registry.terraform.io/hashicorp/aws": "6.59.0"}Cette sortie est la seule preuve de la version réellement utilisée. Le .terraform.lock.hcl la fige ensuite pour toute l'équipe, ce qui évite qu'un collègue résolve une version différente de la vôtre.
Configurer le provider
Section intitulée « Configurer le provider »Créez providers.tf. Voici la version complète, commentée argument par argument :
provider "aws" { region = var.aws_region
default_tags { tags = var.common_tags }}Face à un vrai compte AWS, c'est tout. La région suffit, l'identité venant de votre ~/.aws ou de vos variables d'environnement, et default_tags se charge des étiquettes.
Poser les tags une seule fois
Section intitulée « Poser les tags une seule fois »default_tags applique une carte de tags à toutes les ressources de la racine qui en acceptent. Cela remplace le tags = var.common_tags recopié sur chaque ressource, oublié une fois sur trois, et impossible à auditer.
Un détail de ce mécanisme surprend tout le monde au moins une fois, et il vaut d'être vu tout de suite. Les tags par défaut n'apparaissent jamais dans l'attribut tags de la ressource. Ils ne sont visibles que dans tags_all, qui est la fusion des deux :
terraform show -json | jq '.values.root_module.resources[].values | {tags, tags_all}'{ "tags": null, "tags_all": { "environnement": "formation", "gestion": "terraform", "projet": "lab-terraform" }}La ressource ne déclare aucun tag, et pourtant elle en porte trois. tags reflète ce que la ressource déclare, tags_all reflète ce qu'elle porte vraiment. C'est tags_all qu'il faut regarder pour vérifier une politique d'étiquetage.
Les arguments qu'on découvre trop tard
Section intitulée « Les arguments qu'on découvre trop tard »Le bloc provider en accepte bien d'autres, que la documentation liste mais qu'on ne rencontre qu'au moment d'un incident.
| Argument | Ce qu'il évite |
|---|---|
allowed_account_ids | Appliquer en production une racine destinée au bac à sable |
forbidden_account_ids | La même protection, exprimée par exclusion |
max_retries, retry_mode | Les échecs sur throttling d'API dans les grosses racines |
ignore_tags | Les diffs perpétuels causés par des tags posés par un autre outil |
allowed_account_ids mérite une mention particulière : c'est le garde-fou le moins coûteux contre l'erreur la plus chère, celle du mauvais compte. Le provider vérifie l'identité du compte avant d'agir, et refuse d'appliquer si elle ne figure pas dans la liste.
Viser un émulateur local plutôt qu'AWS
Section intitulée « Viser un émulateur local plutôt qu'AWS »Si vous travaillez sans compte AWS, trois ajouts changent la donne. Le bloc endpoints redirige un service vers une autre adresse, et les arguments qui commencent par skip_ désactivent les validations distantes qui, elles, partiraient interroger le vrai AWS :
provider "aws" { region = var.aws_region access_key = "test" secret_key = "test"
skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true
default_tags { tags = var.common_tags }
endpoints { ec2 = var.emulateur_endpoint }}Ces trois skip_* ne sont pas des raccourcis de confort. Sans eux, le provider valide les identifiants auprès de STS, interroge la metadata API, et réclame l'identifiant du compte, trois appels qui sortiraient vers AWS quelle que soit votre redirection endpoints.
Lire une AMI sans coder son identifiant
Section intitulée « Lire une AMI sans coder son identifiant »Une AMI est le modèle de disque dont part une instance. Son identifiant change selon la région et à chaque publication de l'éditeur : le coder en dur rend la configuration inutilisable ailleurs, et périmée en quelques semaines.
data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical
filter { name = "name" values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"] }
filter { name = "virtualization-type" values = ["hvm"] }}Le point important n'est pas most_recent, c'est owners. Sans restriction de propriétaire, « l'image la plus récente qui s'appelle à peu près comme ça » peut être publiée par n'importe qui : c'est une porte d'entrée classique dans la chaîne d'approvisionnement. owners est ce qui protège, most_recent ne fait que trancher entre les candidates restantes.
Le provider en a d'ailleurs tiré les conséquences. En version 6, most_recent = true sans filtre restreignant le propriétaire ou l'image est refusé :
Error: Most Recent Image Not FilteredNotez que terraform validate passe sur cette configuration : c'est le plan qui échoue, parce que le refus vient du provider, pas de la grammaire HCL.
Créer l'instance
Section intitulée « Créer l'instance »resource "aws_instance" "lab" { ami = data.aws_ami.ubuntu.id instance_type = var.instance_type}Aucun réseau n'est précisé. Sur un compte disposant d'un VPC par défaut dans la région visée, AWS y place l'instance et lui attribue un sous-réseau, ce qui permet à cet exemple de tenir en quatre lignes. Ce VPC par défaut n'est pas garanti : il peut avoir été supprimé, ou absent d'un compte créé par une organisation qui l'interdit. Dans ce cas, l'instance demande un subnet_id explicite, ce que couvre le guide sur les réseaux.
Prouver que l'instance tourne
Section intitulée « Prouver que l'instance tourne »Un apply qui rend « Apply complete » signifie que l'API a accepté la demande, pas que la machine tourne. La distinction est visible dans l'état :
terraform show -json | jq '.values.root_module.resources[] | {address, mode, type, state: .values.instance_state}'{ "address": "aws_instance.lab", "mode": "managed", "type": "aws_instance", "state": "running"}Deux champs valent la lecture. instance_state distingue pending de running : une instance fraîchement créée passe par le premier avant d'atteindre le second. Et mode vaut managed pour une ressource que Terraform crée et détruit, contre data pour une data source qui ne fait que lire, comme l'aws_ami précédente.
Vérifier la configuration du provider elle-même
Section intitulée « Vérifier la configuration du provider elle-même »La représentation JSON du plan expose aussi ce que le provider a réellement reçu, ce qui permet de contrôler une racine sans ouvrir un seul fichier .tf :
terraform plan -out=tfplanterraform show -json tfplan | jq '.configuration.provider_config.aws'Vous y trouvez full_name, le version_constraint déclaré, et les expressions de chaque argument. C'est la base sur laquelle un contrôle automatisé peut vérifier qu'une équipe épingle bien ses providers.
Nettoyage
Section intitulée « Nettoyage »terraform destroy -auto-approveterraform show -json | jq '.values.root_module.resources | length'La seconde commande doit rendre 0. Détruire est la seule façon d'arrêter la facturation : une instance oubliée continue de coûter, y compris arrêtée, pour son volume attaché.
Les pièges
Section intitulée « Les pièges »Voici, rassemblés, les écarts qui coûtent le plus de temps.
terraform validate ne valide pas grand-chose
Section intitulée « terraform validate ne valide pas grand-chose »Il vérifie la grammaire et la cohérence interne, pas ce que le provider accepte. Un provider incomplètement configuré, ou un aws_ami insuffisamment filtré, lui passent sous le nez et sortent en code 0. Seul le plan interroge réellement le provider.
Un tag de la ressource écrase un tag par défaut, en silence
Section intitulée « Un tag de la ressource écrase un tag par défaut, en silence »Si default_tags pose projet = "lab" et que la ressource déclare projet = "autre", la ressource gagne. Aucune erreur, aucun avertissement : la valeur de tags_all est simplement celle de la ressource. C'est le mode d'échec typique d'une politique d'étiquetage qu'on croit appliquée.
Une contrainte sans borne haute est une promesse en l'air
Section intitulée « Une contrainte sans borne haute est une promesse en l'air »version = ">= 6.0" autorise la majeure 7. ~> 6.0 la refuse, et se lit plus vite. L'important est qu'un plafond existe.
AuthFailure ne se soigne pas par les pleins pouvoirs
Section intitulée « AuthFailure ne se soigne pas par les pleins pouvoirs »Face à un refus d'API, la tentation est d'attacher une policy FullAccess et de passer à autre chose. C'est un mauvais réflexe : cela contredit le moindre privilège, et masque la vraie cause, qui est le plus souvent une région, un profil ou un compte différent de celui qu'on croit viser. Lisez le message, vérifiez l'identité effective avec aws sts get-caller-identity, puis n'accordez que la permission manquante.
Les identifiants du poste masquent une configuration incomplète
Section intitulée « Les identifiants du poste masquent une configuration incomplète »Tant que votre ~/.aws existe, une racine sans identité déclarée fonctionne chez vous. Elle cassera en CI. Pour vous en assurer, jouez la racine dans un environnement purgé de toute variable AWS_* et avec un HOME sans .aws : ce qui n'est pas dans la configuration n'existe alors plus.
À retenir
Section intitulée « À retenir »- Le provider résout son identité dans un ordre déterminé, et échoue avant le plan s'il n'en trouve aucune
terraform version -jsondit la version réellement installée, la contrainte dit seulement ce qui est permis- Une contrainte de version se borne des deux côtés
default_tagsn'écrit jamais danstags, seulement danstags_all- Sur une data source
aws_ami, c'estownersqui protège, pasmost_recent instance_statedistingue une instance acceptée d'une instance qui tourne- Détruire reste la seule façon d'arrêter la facturation
Pour aller plus loin
Section intitulée « Pour aller plus loin »- IAM : rôle, policy et instance profile : Le moyen d'autoriser votre EC2 à appeler les API AWS sans y stocker de clé.
- Backend S3 et remote state : La sortie du state local dès que l'instance devient une ressource partagée.
- Launch template et autoscaling : La suite logique quand une instance unique ne suffit plus à absorber la charge.