
Une EC2 ne devrait jamais "tout pouvoir faire" par défaut. Dès qu'une machine doit lire un bucket S3, interroger une API AWS ou décrire d'autres ressources, vous devez choisir exactement quelles permissions lui donner. Sinon, la moindre fuite de credentials ou la moindre erreur de configuration ouvre beaucoup trop de droits. Ce guide introduit donc le trio essentiel d'AWS côté identité : policy, rôle et instance profile.
L'enchaînement paraît abstrait tant qu'on ne sait pas ce que fait chaque objet. La policy est un document JSON qui énumère des actions autorisées ou refusées. Le rôle est une identité IAM sans mot de passe : il porte les policies attachées et une trust policy qui désigne qui a le droit de l'endosser. L'instance profile existe parce qu'une EC2 n'accepte pas un rôle directement : l'API RunInstances ne prend qu'un nom d'instance profile, lequel référence exactement un rôle. Le service de métadonnées de l'instance distribue ensuite des credentials temporaires que le SDK et l'AWS CLI récupèrent tout seuls.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer une policy IAM avec
aws_iam_policy_document(JSON as code) - Créer un rôle IAM avec une trust policy pour EC2
- Attacher une policy à un rôle avec
aws_iam_role_policy_attachment - Créer un instance profile pour lier le rôle à une EC2
- Ajouter le profil à une instance via
iam_instance_profile - Vérifier les ARNs dans les outputs
Pourquoi IAM arrive à ce moment du parcours
Section intitulée « Pourquoi IAM arrive à ce moment du parcours »Après le provider et le réseau, la question suivante est logique : que peut faire l'instance une fois démarrée ? Dans AWS, la réponse passe par IAM. Contrairement à un script local, une EC2 ne devrait jamais embarquer de clés statiques dans ses fichiers.
Ce guide montre donc la bonne direction dès le début : donner à l'instance un rôle ciblé, avec des permissions explicites et réutilisables.
Prérequis
Section intitulée « Prérequis »- Terraform ≥ 1.11 installé
- AWS CLI configuré
- Avoir lu Déployer une première EC2 et Security groups, subnet et instance, ou comprendre
data,resourceetoutputs
Objectif
Section intitulée « Objectif »Vous allez construire les quatre objets dans l'ordre où ils dépendent les uns des autres, car aucun ne se crée isolément : la policy avant le rôle, le rôle avant l'instance profile, et l'instance profile avant l'EC2. Terraform déduit cet ordre tout seul à partir des références entre ressources, ce qui explique qu'aucun depends_on n'apparaisse dans le code de ce guide. Le résultat final tient en quatre éléments :
- Une policy IAM qui permet de lire depuis S3 et décrire des instances
- Un rôle IAM qui assume cette policy
- Un instance profile qui lie le rôle
- Une EC2 qui hérite des permissions via le profil
Durée estimée : 15 minutes Coût : peut être couvert par l'offre gratuite, selon la date de création de votre compte. Détail et vérification juste en dessous.
Quel type d'instance reste dans l'offre gratuite
Section intitulée « Quel type d'instance reste dans l'offre gratuite »L'offre gratuite AWS a changé le 15 juillet 2025, et le type d'instance
à choisir en dépend. Un compte ouvert avant cette date garde t2.micro et
t3.micro ; un compte ouvert depuis n'a plus t2.micro du tout, mais
t3.micro, t3.small, t4g.micro, t4g.small, c7i-flex.large et
m7i-flex.large.
| Compte avant le 15/07/2025 | Compte depuis le 15/07/2025 | |
|---|---|---|
| Types éligibles | t2.micro, t3.micro | t3.micro, t3.small, t4g.micro, t4g.small, c7i-flex.large, m7i-flex.large |
| Durée | 12 mois | 6 mois, ou jusqu'à épuisement des crédits |
| Dépassement | facturé au tarif normal | dépend du plan choisi, voir ci-dessous |
La colonne de droite demande une précision qu'AWS place ailleurs : depuis juillet 2025, tout nouveau compte choisit à l'inscription entre un free plan et un paid plan. Les six mois et l'impossibilité de dépasser ne valent que pour le free plan ; en paid plan, l'usage au-delà des crédits est facturé au tarif normal. Le crédit d'accueil est de 100 USD, porté jusqu'à 200 USD en réalisant les activités proposées.
Ces labs retiennent donc t3.micro, le seul type éligible dans les deux
cas. Plutôt que de faire confiance à une liste qui rebougera, demandez à
votre compte ce à quoi il a droit :
aws ec2 describe-instance-types \ --filters Name=free-tier-eligible,Values=true \ --query "InstanceTypes[*].[InstanceType]" \ --output text | sortUne instance reste facturable dès que vous sortez de ces limites, et le
stockage EBS compte séparément. Détruisez systématiquement le lab avec
terraform destroy une fois l'exercice terminé.
Préparation
Section intitulée « Préparation »Terraform raisonne par répertoire de travail : il charge tous les fichiers .tf du dossier courant et y crée son état. Travaillez donc dans un répertoire vide et dédié, sinon terraform apply prendra en compte des ressources d'un autre projet.
mkdir -p ~/terraform-aws-iam-ec2cd ~/terraform-aws-iam-ec2Étape 1, Déclarer le provider
Section intitulée « Étape 1, Déclarer le provider »Ce fichier verrouille les versions avant tout le reste. required_version refuse d'exécuter le code avec un binaire Terraform trop ancien, et la contrainte ~> 5.0 autorise les mises à jour mineures du provider AWS en retenant le passage à la majeure. Non pas que la 6.x casse cette configuration, le commentaire ci-dessous dit le contraire, mais parce qu'un changement de majeure ne se publie qu'une fois rejoué. La région n'est pas codée en dur : elle vient d'une variable définie à l'étape suivante.
Créez versions.tf :
terraform { required_version = ">= 1.11.0" required_providers { aws = { source = "hashicorp/aws" # Série 5.x assumée. La 6.x est la série courante : la configuration # de ce guide y est VALIDE, vérifié le 2026-09-22 par un # `terraform init` puis `validate` en 6.66.0. La contrainte ne bouge # pas tant que le cycle complet (plan, apply, destroy) n'a pas été # rejoué sur un compte réel. version = "~> 5.0" } }}
provider "aws" { region = var.aws_region}Étape 2, Déclarer les variables
Section intitulée « Étape 2, Déclarer les variables »Chaque variable porte une valeur par défaut, donc le code s'applique tel quel sans fichier de valeurs. Deux d'entre elles méritent votre attention : role_name doit être unique dans le compte AWS, sinon la création échoue, et instance_type conditionne la facturation, t3.micro restant dans l'offre gratuite.
Créez variables.tf :
variable "aws_region" { description = "AWS region" type = string default = "us-east-1"}
variable "instance_name" { description = "Name tag for the EC2 instance" type = string default = "lab03-instance-with-iam"}
variable "instance_type" { # t3.micro est eligible a l'offre gratuite quelle que soit la date de # creation du compte, contrairement a t2.micro. description = "Type d'instance EC2" type = string default = "t3.micro"}
variable "role_name" { description = "Name of the IAM role" type = string default = "lab03-ec2-role"}Étape 3, Créer la policy document
Section intitulée « Étape 3, Créer la policy document »La data source aws_iam_policy_document ne crée rien dans AWS : elle assemble un document JSON que d'autres ressources consommeront. Chaque bloc statement devient une entrée du tableau Statement, et son attribut .json contient le résultat rendu. Un détail sur le second statement : l'action ec2:DescribeInstances ne gère pas les permissions au niveau ressource côté AWS, d'où le resources = ["*"] qui n'est pas ici une négligence.
Créez main.tf avec cette data source :
# Créer une policy IAM document (JSON policy as code)data "aws_iam_policy_document" "s3_read_policy" { statement { effect = "Allow" actions = [ "s3:GetObject", "s3:ListBucket" ] resources = [ "arn:aws:s3:::*", "arn:aws:s3:::*/*" ] }
statement { effect = "Allow" actions = ["ec2:DescribeInstances"] resources = ["*"] }}Ce bloc définit :
- Statement 1 : Lire depuis S3 (
ListBucket,GetObject) sur tous les buckets - Statement 2 : Décrire les instances EC2 (lire les métadonnées)
La data source génère automatiquement le JSON, pas besoin de l'écrire manuellement.
Étape 4, Créer le rôle IAM
Section intitulée « Étape 4, Créer le rôle IAM »Le rôle se crée d'abord vide de permissions : l'unique policy qu'il porte à ce stade est sa trust policy, celle qui répond à « qui peut endosser ce rôle ». Ici, le principal est le service ec2.amazonaws.com, ce qui autorise n'importe quelle instance de votre compte à demander les credentials du rôle, à condition qu'elle en reçoive l'instance profile. Sans ce bloc, l'appel sts:AssumeRole échouerait.
Toujours dans main.tf, ajoutez :
resource "aws_iam_role" "lab03_role" { name = var.role_name
assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } Action = "sts:AssumeRole" } ] })
tags = { Name = var.role_name }}Ce bloc crée :
- Un rôle IAM = conteneur de permissions
- Une trust policy = qui peut utiliser ce rôle ?
Principal: { Service: "ec2.amazonaws.com" }= Les instances EC2 peuvent assumer ce rôleAction: "sts:AssumeRole"= Permission pour les EC2 de prendre le rôle
Étape 5, Créer la policy IAM resource
Section intitulée « Étape 5, Créer la policy IAM resource »Cette étape transforme le document généré à l'étape 3 en objet réel dans AWS, avec son propre ARN. C'est cet ARN qui rend la policy réutilisable : d'autres rôles pourront s'y attacher sans dupliquer le JSON. Le name doit être unique dans le compte, comme celui du rôle.
Toujours dans main.tf, ajoutez :
resource "aws_iam_policy" "s3_read_policy" { name = "lab03-s3-read-policy" description = "Allow EC2 to read from S3 and describe instances" policy = data.aws_iam_policy_document.s3_read_policy.json
tags = { Name = "lab03-s3-read-policy" }}Ce bloc :
- Crée une policy managée AWS à partir du document défini plus haut
policy = data.aws_iam_policy_document.s3_read_policy.json: utilise le JSON généré
Étape 6, Attacher la policy au rôle
Section intitulée « Étape 6, Attacher la policy au rôle »L'attachement est une ressource à part entière, pas un argument du rôle. Cette séparation permet de détacher une policy sans détruire le rôle, et d'attacher la même policy à plusieurs rôles. Notez que la ressource référence le rôle par son name et la policy par son arn : ce n'est pas une incohérence, c'est ce qu'attend l'API IAM.
Toujours dans main.tf, ajoutez :
resource "aws_iam_role_policy_attachment" "attach_s3_policy" { role = aws_iam_role.lab03_role.name policy_arn = aws_iam_policy.s3_read_policy.arn}Ce bloc dit :
- « Attache la policy
s3_read_policyau rôlelab03_role» - L'EC2 pourra maintenant faire ce que la policy autorise
Étape 7, Créer l'instance profile
Section intitulée « Étape 7, Créer l'instance profile »C'est l'étape que l'on oublie, parce qu'elle n'a pas d'équivalent visible dans la console AWS : celle-ci crée l'instance profile en arrière-plan quand vous associez un rôle à une instance. En Terraform, rien n'est implicite, il faut donc la déclarer. Un instance profile ne référence qu'un seul rôle, et son nom est ce que l'EC2 recevra à l'étape suivante.
Toujours dans main.tf, ajoutez :
resource "aws_iam_instance_profile" "lab03_profile" { name = "lab03-ec2-profile" role = aws_iam_role.lab03_role.name}L'instance profile :
- Est le lien entre rôle IAM et EC2
- Contient exactement un rôle, limite qu'AWS ne relève pas ; un même rôle peut en revanche figurer dans plusieurs instance profiles
- Est attaché à une instance, pas directement au rôle
Étape 8, Créer l'EC2 avec le profil
Section intitulée « Étape 8, Créer l'EC2 avec le profil »Dernière étape, celle qui relie tout. La data source aws_ami évite de coder un identifiant d'AMI en dur, qui change à chaque région et à chaque publication d'image ; most_recent = true retient la plus récente parmi celles qui correspondent au filtre, et owners restreint la recherche au compte officiel de Canonical pour ne pas récupérer une image publique tierce. L'argument iam_instance_profile attend le nom du profil, pas son ARN.
Toujours dans main.tf, ajoutez :
# Lire l'AMI Ubuntu la plus récentedata "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical (Ubuntu)
filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }
filter { name = "virtualization-type" values = ["hvm"] }}
resource "aws_instance" "lab03_vm" { ami = data.aws_ami.ubuntu.id instance_type = var.instance_type iam_instance_profile = aws_iam_instance_profile.lab03_profile.name
tags = { Name = var.instance_name }}Clé :
iam_instance_profile = aws_iam_instance_profile.lab03_profile.name: l'EC2 hérite des permissions du profil
Étape 9, Définir les outputs
Section intitulée « Étape 9, Définir les outputs »Ces sorties ne sont pas décoratives : elles servent de vérification. En affichant l'ARN du rôle, celui du profil et surtout instance_iam_instance_profile, vous prouvez que l'association a eu lieu côté instance, sans ouvrir la console. Les outputs sont aussi ce qu'une autre stack lira via terraform_remote_state.
Créez outputs.tf :
output "iam_role_arn" { description = "ARN of the IAM role" value = aws_iam_role.lab03_role.arn}
output "iam_role_name" { description = "Name of the IAM role" value = aws_iam_role.lab03_role.name}
output "iam_policy_arn" { description = "ARN of the IAM policy" value = aws_iam_policy.s3_read_policy.arn}
output "instance_profile_arn" { description = "ARN of the instance profile" value = aws_iam_instance_profile.lab03_profile.arn}
output "instance_id" { description = "EC2 Instance ID" value = aws_instance.lab03_vm.id}
output "instance_arn" { description = "EC2 Instance ARN" value = aws_instance.lab03_vm.arn}
output "instance_iam_instance_profile" { description = "IAM instance profile name attached to the instance" value = aws_instance.lab03_vm.iam_instance_profile}Étape 10, Valeurs des variables
Section intitulée « Étape 10, Valeurs des variables »Terraform charge automatiquement terraform.tfvars s'il existe, sans option de ligne de commande. Ce fichier reprend ici les valeurs par défaut, ce qui rend le paramétrage visible en un seul endroit quand vous changez de région ou de nom de rôle. Ne le committez pas s'il finit par contenir des valeurs propres à un compte.
Créez terraform.tfvars :
aws_region = "us-east-1"instance_name = "lab03-instance-with-iam"instance_type = "t3.micro"role_name = "lab03-ec2-role"Vérification
Section intitulée « Vérification »Les trois premières commandes forment la séquence standard : init télécharge le provider AWS, validate contrôle la syntaxe et les références sans appeler AWS, apply crée réellement les cinq ressources. L'option -auto-approve saute la confirmation manuelle, ce qui convient à un lab mais reste à proscrire sur une infrastructure réelle. Les deux commandes AWS CLI de la dernière étape confirment côté fournisseur ce que les outputs annoncent côté Terraform.
-
Initialiser :
Fenêtre de terminal terraform init -
Valider :
Fenêtre de terminal terraform validate -
Appliquer :
Fenêtre de terminal terraform apply -auto-approveSortie attendue (fin) :
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.Outputs:iam_policy_arn = "arn:aws:iam::123456789012:policy/lab03-s3-read-policy"iam_role_arn = "arn:aws:iam::123456789012:role/lab03-ec2-role"iam_role_name = "lab03-ec2-role"instance_arn = "arn:aws:ec2:us-east-1:123456789012:instance/i-046ec132d2c5984af"instance_iam_instance_profile = "lab03-ec2-profile"instance_id = "i-046ec132d2c5984af"instance_profile_arn = "arn:aws:iam::123456789012:instance-profile/lab03-ec2-profile" -
Vérifier dans la console AWS (optionnel) :
Fenêtre de terminal # Voir le rôle crééaws iam get-role --role-name lab03-ec2-role# Voir l'instance profileaws iam get-instance-profile --instance-profile-name lab03-ec2-profile
Anatomie
Section intitulée « Anatomie »Maintenant que l'infrastructure existe, revenons sur les deux points qui provoquent le plus d'erreurs quand on écrit sa propre policy : la confusion entre les deux natures de policy attachées à un rôle, et l'intérêt réel de générer le JSON depuis HCL.
Trust policy et policy de permissions
Section intitulée « Trust policy et policy de permissions »Un rôle porte deux catégories de policy qui ne répondent pas à la même question, et les inverser produit une erreur difficile à lire. La trust policy est déclarée dans assume_role_policy ; les policies de permissions sont attachées séparément, comme à l'étape 6.
| Type | Question à laquelle elle répond | Où elle se déclare |
|---|---|---|
| Trust policy | qui peut endosser ce rôle ? | l'argument assume_role_policy du rôle |
| Policy de permissions | quelles actions le rôle autorise-t-il ? | une ressource aws_iam_policy, attachée au rôle |
Le vocabulaire d'AWS ajoute une nuance qui déroute, et qu'il vaut mieux connaître avant de lire sa documentation : la trust policy est une resource-based policy, puisqu'elle est portée par le rôle lui-même, tandis que la policy S3 de l'étape 6 est une identity-based policy, attachée à une identité. Dans ce lab, les deux questions reçoivent des réponses distinctes :
- Trust policy du rôle : « les EC2 peuvent l'assumer »
- Policy de permissions, la policy S3 : « lire depuis S3 et décrire les instances »
aws_iam_policy_document
Section intitulée « aws_iam_policy_document »Cette data source génère du JSON IAM à partir de HCL lisible. Sans elle, il faudrait écrire ce document à la main :
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject", "s3:ListBucket"], "Resource": ["arn:aws:s3:::*", "arn:aws:s3:::*/*"] } ]}Avec aws_iam_policy_document, c'est généré automatiquement et testable.
Bonnes pratiques
Section intitulée « Bonnes pratiques »Le code de ce guide privilégie la lisibilité pédagogique, pas la rigueur d'un compte de production : la policy S3 y couvre tous les buckets. Les quatre règles ci-dessous montrent comment resserrer ce périmètre avant de réutiliser ce code ailleurs.
1. Principe du moindre privilège
Section intitulée « 1. Principe du moindre privilège »Une policy s'écrit en listant les actions dont l'application a besoin, jamais en partant de * pour retirer ensuite. IAM refuse par défaut : tout ce que vous n'accordez pas explicitement est déjà interdit, il n'y a donc rien à gagner à élargir « au cas où ».
# ❌ Trop permissifactions = ["*"]
# ✅ Spécifiqueactions = [ "s3:GetObject", "s3:ListBucket"]2. Restreindre les ressources
Section intitulée « 2. Restreindre les ressources »Limiter les actions ne suffit pas si elles s'appliquent à tout le compte. Sur S3, il faut deux ARN distincts : celui du bucket pour ListBucket, et celui des objets, suffixé par /*, pour GetObject. Oublier le second donne une erreur d'accès sur les objets alors que la liste fonctionne.
# ❌ Toutresources = ["*"]
# ✅ Spécifiqueresources = [ "arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]3. Utiliser des rôles pour chaque service
Section intitulée « 3. Utiliser des rôles pour chaque service »Un rôle partagé finit toujours par accumuler les permissions de tous ses usages, et plus personne n'ose en retirer une. Un rôle par service garde un périmètre lisible et rend la révocation possible : supprimer le rôle n'affecte que la charge de travail concernée.
# ❌ Un rôle pour toutrole_name = "general-role"
# ✅ Rôles spécialisésresource "aws_iam_role" "ec2_s3_role" { ... }resource "aws_iam_role" "lambda_dynamo_role" { ... }4. Documenter les policies
Section intitulée « 4. Documenter les policies »Une policy sans explication devient intouchable au bout de six mois, faute de savoir quel service en dépend. Renseignez l'argument description de la ressource aws_iam_policy, comme à l'étape 5, et commentez les statement dont l'intention n'est pas évidente.
Attention au bon endroit : la data source n'accepte pas d'argument description, terraform validate répond An argument named "description" is not expected here. L'intention se documente donc par un commentaire et par le sid de chaque statement, la description allant sur la ressource aws_iam_policy.
data "aws_iam_policy_document" "example" { # Le commentaire porte l'intention : la data source n'a pas de description statement { sid = "ReadProdBucketOnly" actions = ["s3:GetObject"] resources = ["arn:aws:s3:::prod-data/*"] }}
resource "aws_iam_policy" "example" { name = "lab03-s3-read-prod" description = "Lecture seule du bucket prod-data, consommée par le rôle EC2" policy = data.aws_iam_policy_document.example.json}Dépannage
Section intitulée « Dépannage »Deux familles d'erreurs dominent : les permissions de l'utilisateur qui exécute Terraform, qui n'a pas forcément le droit de créer des rôles, et les collisions de noms, IAM refusant deux rôles homonymes dans un même compte. Le dernier cas du tableau est plus sournois, car terraform apply réussit alors que l'instance ne peut rien faire.
| Symptôme | Cause | Solution |
|---|---|---|
Error: User is not authorized to perform: iam:CreateRole | Utilisateur AWS sans permissions IAM | Demander IAMFullAccess ou permissions iam:CreateRole, iam:CreatePolicy, etc. |
Error: EntityAlreadyExists: Role with name lab03-ec2-role already exists | Rôle existe déjà | Changer role_name ou terraform destroy d'abord |
| EC2 créée mais profil absent | Timing ou oubli iam_instance_profile | Ajouter iam_instance_profile = aws_iam_instance_profile.lab03_profile.name |
| Policy attachée mais EC2 ne peut pas lire S3 | Trust policy manquante | Vérifier que assume_role_policy permet EC2 |
Nettoyage
Section intitulée « Nettoyage »L'instance EC2 est facturée tant qu'elle tourne, même sur un type éligible à l'offre gratuite, dès que le quota mensuel est dépassé. Détruisez donc l'ensemble dès la fin du lab : Terraform supprime les cinq ressources dans l'ordre inverse de leur création, en détachant la policy avant de supprimer le rôle. L'opération est irréversible.
terraform destroy -auto-approveSortie attendue :
Destroy complete! Resources: 5 destroyed.Nettoyez les fichiers :
rm -rf .terraform* terraform.tfstate*Mettre en pratique
Section intitulée « Mettre en pratique »aws_iam_policy_document est une data source locale : elle n'interroge rien, elle fabrique du JSON. Vous composez la chaîne complète, policy, rôle, instance profile, instance, puis vous prouvez dans l'état structuré que la trust policy et les permissions empruntent deux chemins distincts, confusion qui coûte cher en production.
À retenir
Section intitulée « À retenir »- IAM = sécurité AWS, toujours utiliser le moindre privilège
- Policy document génère du JSON depuis du HCL lisible
- Rôle IAM = conteneur de permissions avec trust policy
- Instance profile = lien entre rôle et EC2
- Trust policy répond « qui peut endosser ce rôle ? », et c'est une resource-based policy
- Policy de permissions répond « quelles actions sont autorisées ? », et c'est une identity-based policy
- Attacher plutôt que d'intégrer : utilisez
aws_iam_role_policy_attachmentpour la réutilisabilité
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Launch template et autoscaling : Le rôle IAM construit ici s'attache au launch template qui alimente un groupe d'autoscaling.
- Import, moved et drift : La méthode pour reprendre sous Terraform un rôle ou une policy déjà créés dans la console.