Exercice 09 : Infrastructure as Code avec CloudFormation
🎯 Objectifs
À la fin de cet exercice, vous serez capable de :
- ✅ Écrire un template CloudFormation YAML décrivant une instance EC2 + Security Group
- ✅ Déployer, mettre à jour et supprimer une stack
- ✅ Utiliser les paramètres pour rendre le template réutilisable
- ✅ Utiliser les outputs pour exposer les valeurs créées
Durée estimée : 50 minutes
Difficulté : ⭐⭐⭐⭐☆ (Avancé)
Prérequis : EC2 (Exercice 04), S3 (Exercice 05), notions YAML
📖 Contexte
Cliquer dans la console AWS est excellent pour apprendre, mais pour la production il faut de la reproductibilité : déployer la même infrastructure dans 10 régions, ou la recréer identique après une suppression accidentelle. CloudFormation est l'outil AWS natif d'Infrastructure as Code : vous décrivez ce que vous voulez (état cible), CloudFormation crée et orchestre les ressources dans le bon ordre.
📋 Énoncé
Créez un template CloudFormation qui déploie un serveur web EC2 complet, avec paramètres et outputs.
🧭 Déroulement de l'exercice
Tâche 1 : Créer le template CloudFormation
cat > stack-web-server.yaml << 'EOF'
AWSTemplateFormatVersion: "2010-09-09"
Description: "Stack CloudFormation - Serveur web EC2 avec Security Group"
# ========================
# PARAMÈTRES (personnalisables au déploiement)
# ========================
Parameters:
NomEnvironnement:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Description: "Environnement de déploiement"
TypeInstance:
Type: String
Default: t2.micro
AllowedValues: [t2.micro, t2.small, t2.medium]
Description: "Type d'instance EC2"
NomCleSSH:
Type: AWS::EC2::KeyPair::KeyName
Description: "Nom de la paire de clés SSH pour l'accès à l'instance"
MonIP:
Type: String
Default: "0.0.0.0/0"
Description: "Votre IP pour restreindre l'accès SSH (ex: 203.0.113.0/32)"
# ========================
# MAPPINGS (valeurs par région)
# ========================
Mappings:
RegionToAMI:
eu-west-3:
AMI: ami-0d3c032f5934e1b41 # Amazon Linux 2023 Paris
eu-west-1:
AMI: ami-0905a3c97561e0b69 # Amazon Linux 2023 Dublin
us-east-1:
AMI: ami-0c101f26f147fa7fd # Amazon Linux 2023 N.Virginia
# ========================
# RESSOURCES
# ========================
Resources:
# Security Group
SecurityGroupWeb:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub "sg-web-${NomEnvironnement}"
GroupDescription: !Sub "Security Group pour le serveur web - ${NomEnvironnement}"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: "Trafic HTTP public"
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: "Trafic HTTPS public"
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref MonIP
Description: "SSH depuis mon IP seulement"
Tags:
- Key: Name
Value: !Sub "sg-web-${NomEnvironnement}"
- Key: Environnement
Value: !Ref NomEnvironnement
# Instance EC2
InstanceEC2:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref TypeInstance
ImageId: !FindInMap [RegionToAMI, !Ref "AWS::Region", AMI]
KeyName: !Ref NomCleSSH
SecurityGroups:
- !Ref SecurityGroupWeb # Référence à la ressource ci-dessus
Tags:
- Key: Name
Value: !Sub "serveur-web-${NomEnvironnement}"
- Key: Environnement
Value: !Ref NomEnvironnement
# UserData : script exécuté au premier démarrage
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -e
dnf update -y
dnf install -y nginx
systemctl enable --now nginx
cat > /usr/share/nginx/html/index.html << 'HTML'
<!DOCTYPE html>
<html>
<head><title>CloudFormation Demo</title></head>
<body>
<h1>Déployé par CloudFormation</h1>
<p>Environnement : ${NomEnvironnement}</p>
</body>
</html>
HTML
# ========================
# OUTPUTS (valeurs exportées)
# ========================
Outputs:
IPPubliqueInstance:
Description: "Adresse IP publique de l'instance EC2"
Value: !GetAtt InstanceEC2.PublicIp
Export:
Name: !Sub "${AWS::StackName}-PublicIP"
URLServeurWeb:
Description: "URL du serveur web"
Value: !Sub "http://${InstanceEC2.PublicDnsName}"
IDSecurityGroup:
Description: "ID du Security Group créé"
Value: !GetAtt SecurityGroupWeb.GroupId
Export:
Name: !Sub "${AWS::StackName}-SGID"
EOFTâche 2 : Déployer la stack
STACK_NAME="serveur-web-dev"
# Valider le template avant le déploiement
aws cloudformation validate-template \
--template-body file://stack-web-server.yaml
# Déployer la stack
aws cloudformation create-stack \
--stack-name $STACK_NAME \
--template-body file://stack-web-server.yaml \
--parameters \
ParameterKey=NomEnvironnement,ParameterValue=dev \
ParameterKey=TypeInstance,ParameterValue=t2.micro \
ParameterKey=NomCleSSH,ParameterValue=devops-key \
ParameterKey=MonIP,ParameterValue=$(curl -s https://checkip.amazonaws.com)/32
# Suivre le déploiement
aws cloudformation wait stack-create-complete --stack-name $STACK_NAME
aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--query 'Stacks[0].StackStatus'# Voir les outputs (IP, URL...)
aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--query 'Stacks[0].Outputs' \
--output table
# Tester le serveur web
IP=$(aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--query 'Stacks[0].Outputs[?OutputKey==`IPPubliqueInstance`].OutputValue' \
--output text)
curl http://$IPTâche 3 : Mettre à jour la stack avec un Changeset
Un changeset vous montre exactement ce qui sera modifié avant d'appliquer la mise à jour.
# Modifier le template : ajouter une règle ICMP (ping)
# Ajouter dans SecurityGroupIngress :
cat >> /tmp/patch-sg.txt << 'EOF'
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
Description: "ICMP (ping)"
EOF
# Créer un changeset (ne déploie PAS encore)
aws cloudformation create-change-set \
--stack-name $STACK_NAME \
--template-body file://stack-web-server.yaml \
--change-set-name "ajout-icmp" \
--parameters \
ParameterKey=NomEnvironnement,UsePreviousValue=true \
ParameterKey=TypeInstance,UsePreviousValue=true \
ParameterKey=NomCleSSH,UsePreviousValue=true \
ParameterKey=MonIP,UsePreviousValue=true
# Voir les changements proposés
aws cloudformation describe-change-set \
--stack-name $STACK_NAME \
--change-set-name "ajout-icmp" \
--query 'Changes[*].[ResourceChange.Action,ResourceChange.ResourceType,ResourceChange.PhysicalResourceId]' \
--output table
# Appliquer le changeset
aws cloudformation execute-change-set \
--stack-name $STACK_NAME \
--change-set-name "ajout-icmp"Tâche 4 : Supprimer la stack
# CloudFormation supprime toutes les ressources dans le bon ordre
aws cloudformation delete-stack --stack-name $STACK_NAME
# Attendre la suppression
aws cloudformation wait stack-delete-complete --stack-name $STACK_NAME
echo "Stack supprimée avec succès"Indice : Si la suppression échoue (par exemple, un bucket S3 non vide), CloudFormation laisse la stack en état
DELETE_FAILEDet vous dit quelles ressources n'ont pas pu être supprimées. Vous devez les supprimer manuellement puis relancer la suppression.
✅ Vérification du résultat
aws cloudformation validate-templateretourne sans erreur- La stack passe à l'état
CREATE_COMPLETE curl http://$IPaffiche la page "Déployé par CloudFormation"- Le changeset montre bien la modification du Security Group avant application
- La stack est supprimée et toutes les ressources sont effacées
💡 À retenir
Avantages CloudFormation vs console manuelle :
| Console | CloudFormation | |
|---|---|---|
| Reproductible | ❌ | ✅ |
| Versionnable (Git) | ❌ | ✅ |
| Rollback automatique | ❌ | ✅ |
| Dépendances gérées | ❌ | ✅ |
| Audit/traçabilité | Partiel | ✅ |
Références CloudFormation essentielles :
!Ref NomParametre: valeur d'un paramètre!GetAtt Ressource.Attribut: attribut d'une ressource créée!Sub "texte-${Variable}": substitution de chaîne!FindInMap [Mapping, Clé, SousClé]: lookup dans un mapping
✨ Solution Complète
# Cycle de vie complet d'une stack
aws cloudformation validate-template --template-body file://stack.yaml
aws cloudformation deploy --template-file stack.yaml --stack-name ma-stack \
--parameter-overrides Env=dev --capabilities CAPABILITY_IAM
aws cloudformation describe-stacks --stack-name ma-stack --query 'Stacks[0].Outputs'
aws cloudformation delete-stack --stack-name ma-stack