Aller au contenu
Infrastructure as Code medium

Terraform AWS, Rôle IAM et instance profile

30 min de lecture

logo terraform

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.

  • 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

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.

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 :

  1. Une policy IAM qui permet de lire depuis S3 et décrire des instances
  2. Un rôle IAM qui assume cette policy
  3. Un instance profile qui lie le rôle
  4. Une EC2 qui hérite des permissions via le profil

Durée estimée : 15 minutes Coût : Gratuit (tier gratuit AWS)

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.

Fenêtre de terminal
mkdir -p ~/terraform-aws-iam-ec2
cd ~/terraform-aws-iam-ec2

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
}

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

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.

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ôle
    • Action: "sts:AssumeRole" = Permission pour les EC2 de prendre le rôle

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é

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_policy au rôle lab03_role"
  • L'EC2 pourra maintenant faire ce que la policy autorise

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

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écente
data "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

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
}

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"

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.

  1. Initialiser :

    Fenêtre de terminal
    terraform init
  2. Valider :

    Fenêtre de terminal
    terraform validate
  3. Appliquer :

    Fenêtre de terminal
    terraform apply -auto-approve

    Sortie 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"
  4. 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 profile
    aws iam get-instance-profile --instance-profile-name lab03-ec2-profile

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.

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.

TypeRôleExemple
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"

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.

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.

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 permissif
actions = ["*"]
# ✅ Spécifique
actions = [
"s3:GetObject",
"s3:ListBucket"
]

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.

# ❌ Tout
resources = ["*"]
# ✅ Spécifique
resources = [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]

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 tout
role_name = "general-role"
# ✅ Rôles spécialisés
resource "aws_iam_role" "ec2_s3_role" { ... }
resource "aws_iam_role" "lambda_dynamo_role" { ... }

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
}

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ômeCauseSolution
Error: User is not authorized to perform: iam:CreateRoleUtilisateur AWS sans permissions IAMDemander IAMFullAccess ou permissions iam:CreateRole, iam:CreatePolicy, etc.
Error: EntityAlreadyExists: Role with name lab03-ec2-role already existsRôle existe déjàChanger role_name ou terraform destroy d'abord
EC2 créée mais profil absentTiming ou oubli iam_instance_profileAjouter iam_instance_profile = aws_iam_instance_profile.lab03_profile.name
Policy attachée mais EC2 ne peut pas lire S3Trust policy manquanteVérifier que assume_role_policy permet EC2

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.

Fenêtre de terminal
terraform destroy -auto-approve

Sortie attendue :

Destroy complete! Resources: 5 destroyed.

Nettoyez les fichiers :

Fenêtre de terminal
rm -rf .terraform* terraform.tfstate*
  1. IAM = sécurité AWS, toujours utiliser le moindre privilège
  2. Policy document genère du JSON depuis du HCL lisible
  3. Rôle IAM = conteneur de permissions avec trust policy
  4. Instance profile = lien entre rôle et EC2
  5. Trust policy répond "Qui peut utiliser ce rôle ?"
  6. Resource policy répond "Qu'est-ce qui est autorisé ?"
  7. Attacher plutôt que d'intégrer : utilisez aws_iam_role_policy_attachment pour la réutilisabilité

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn