
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 police IAM avec
aws_iam_policy_document(JSON as code) - Créer un rôle IAM avec une trust policy pour EC2
- Attacher une police à 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 ou à un environnement de test bricolé, une EC2 ne devrait pas embarquer des 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 : Gratuit (tier gratuit AWS)
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 mais bloque le passage à une version majeure, qui casserait des noms d'arguments. 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" 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, t2.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" { description = "EC2 instance type" type = string default = "t2.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 à connaître 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 à la question « 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'EC2 obtiendrait un rôle qu'elle n'a pas le droit d'utiliser et 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 les débutants oublient, 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 un ou plusieurs rôles (généralement un seul)
- 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 bien 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 n'a l'air de rien mais 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 = "t2.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 la cohérence des 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::276757567417:policy/lab03-s3-read-policy"iam_role_arn = "arn:aws:iam::276757567417:role/lab03-ec2-role"iam_role_name = "lab03-ec2-role"instance_arn = "arn:aws:ec2:us-east-1:276757567417:instance/i-046ec132d2c5984af"instance_iam_instance_profile = "lab03-ec2-profile"instance_id = "i-046ec132d2c5984af"instance_profile_arn = "arn:aws:iam::276757567417: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 vs Resource Policy
Section intitulée « Trust Policy vs Resource Policy »Un rôle porte deux catégories de policy qui ne répondent pas à la même question, et inverser les deux 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 | Rôle | Exemple |
|---|---|---|
| Trust policy | "Qui peut utiliser ce rôle ?" | EC2 service, Lambda, utilisateurs |
| Resource policy | "Quelles actions sont autorisées ?" | S3 read, EC2 describe |
Dans ce lab :
- Trust policy du rôle : "les EC2 peuvent l'assumer"
- Resource policy (la policy S3) : "lire depuis S3 et décrire instances"
aws_iam_policy_document
Section intitulée « aws_iam_policy_document »Cet data source génère du JSON IAM à partir de HCL lisible. Avant :
{ "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 sur ce lab : 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 au-delà du quota mensuel. 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, le rôle et la policy ne se récupèrent pas.
terraform destroy -auto-approveSortie attendue :
Destroy complete! Resources: 5 destroyed.Nettoyez les fichiers :
rm -rf .terraform* terraform.tfstate*À retenir
Section intitulée « À retenir »- IAM = sécurité AWS, toujours utiliser le moindre privilège
- Policy document genè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 utiliser ce rôle ?"
- Resource policy répond "Qu'est-ce qui est autorisé ?"
- 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.