Exercises 3 & 4 : Ansible Idempotency & Multi-Instance Deployment
Exercise 3 : Test d’idempotence
Objectif
Relancer le playbook configure_sample_app_playbook.yml une deuxième fois et observer quel comportement est produit.
Concept : Idempotence dans Ansible
Définition : Un playbook est idempotent s’il peut être relancé plusieurs fois sans changements d’état indésirables du système. Cela signifie :
- Tasks marquées
changed_when: falseoustate: present(idempotentes) ne signalent PAS de changement à la deuxième exécution. - Tasks sans gestion de l’idempotence (comme
shellbrutes) peuvent TOUJOURS signaler un changement.
Analyse du rôle sample-app/tasks/main.yml
Task 1 : Install Node.js setup repository
- name: Install Node.js setup repository
shell: curl -fsSL https://rpm.nodesource.com/setup_21.x | bash -
args:
creates: /etc/yum.repos.d/nodesource-el8.repo
changed_when: falseComportement idempotent ? OUI
creates:— si le fichier existe, la tâche ne s’exécute pas.changed_when: false— ne marque jamais comme changé (même si relanc).- Deuxième exécution :
skipped(car le fichier existe déjà).
Task 2 : Install Node.js
- name: Install Node.js
yum:
name: nodejs
state: presentComportement idempotent ? OUI
state: present— moduleyumest conçu pour être idempotent.- Si le paquet est déjà installé, il ne fait rien et marque
ok(pas de changement). - Deuxième exécution :
ok(car nodejs est déjà installé).
Task 3 : Copy sample app
- name: Copy sample app
copy:
src: app.js
dest: /home/ec2-user/app.js
owner: ec2-user
group: ec2-user
mode: '0755'Comportement idempotent ? OUI
- Module
copycompare le contenu (checksums). Si le fichier existe et le contenu est identique, marqueok. - Deuxième exécution :
ok(sauf si le fichierapp.jssource a changé).
Task 4 : Check if app is already running
- name: Check if app is already running
shell: pgrep -f "node /home/ec2-user/app.js"
register: app_running
ignore_errors: yes
changed_when: falseComportement idempotent ? OUI
- Utilise une commande non-destructive (
pgrep). changed_when: false— ne marque jamais de changement.- Deuxième exécution :
okavecapp_running.rc= 0 (processus trouvé).
Task 5 : Stop any existing app
- name: Stop any existing app
shell: pkill -f "node /home/ec2-user/app.js" || true
when: app_running.rc == 0
changed_when: falseComportement idempotent ? PARTIEL
changed_when: false— ne marque pas de changement.- Deuxième exécution :
skipped(carapp_running.rc != 0après la première exécution qui a arrêté le processus). - ⚠️ Problème : la première fois elle arrête l’app, mais elle ne remarque pas si l’arrêt a échoué.
Task 6 : Start sample app
- name: Start sample app
shell: nohup node /home/ec2-user/app.js > /tmp/app.log 2>&1 &
args:
chdir: /home/ec2-user/
become_user: ec2-userComportement idempotent ? NON (problématique)
shellbrute sans condition.- À chaque exécution, relance le
nohup→ crée un nouveau processus (dupliquant potentiellement les instances). - Deuxième exécution :
changed(car la commande s’exécute toujours). - ⚠️ Risque : plusieurs instances de
nodeen écoute sur le même port → conflit.
Résultat attendu à la deuxième exécution
TASK [sample-app : Install Node.js setup repository] ... skipped
TASK [sample-app : Install Node.js] ..................... ok (no changes)
TASK [sample-app : Copy sample app] ..................... ok (no changes)
TASK [sample-app : Check if app is already running] .... ok (app found)
TASK [sample-app : Stop any existing app] .............. skipped (condition: False)
TASK [sample-app : Start sample app] ................... changed (launched again)
Améliorations pour une meilleure idempotence
Problème identifié : La tâche “Start sample app” n’est pas idempotente. Elle redémarre l’app à chaque fois.
Solution 1 : Conditionner le démarrage
- name: Start sample app (if not running)
shell: nohup node /home/ec2-user/app.js > /tmp/app.log 2>&1 &
when: app_running.rc != 0 # Ne lancer que si pas déjà en cours
changed_when: falseSolution 2 : Utiliser un service systemd (meilleur) Créer un fichier service pour gérer l’app comme un vrai service :
- name: Create systemd service for app
copy:
dest: /etc/systemd/system/sample-app.service
content: |
[Unit]
Description=Sample Node.js App
After=network.target
[Service]
Type=simple
User=ec2-user
ExecStart=/usr/bin/node /home/ec2-user/app.js
Restart=on-failure
[Install]
WantedBy=multi-user.target
- name: Start and enable service
systemd:
name: sample-app
state: started
enabled: yes
daemon_reload: yesAvec cette approche, redémarrer le playbook ne crée pas de doublons — systemd gère l’idempotence.
Exercise 4 : Déployer sur plusieurs instances
Objectif
Modifier le playbook de création pour lancer 2 ou 3 instances en parallèle et mettre à jour l’inventaire pour les découvrir toutes.
Approche 1 : Modifier le playbook de création
Fichier : create_ec2_instance_playbook.yml (version multi-instance)
- name: Deploy multiple EC2 instances in AWS
hosts: localhost
gather_facts: no
collections:
- amazon.aws
vars:
instance_type: t3.micro
image_id: ami-0900fe555666598a2
instance_count: 3 # Nombre d'instances à créer
environment:
AWS_REGION: us-east-2
tasks:
- name: Create security group
amazon.aws.ec2_security_group:
name: sample-app-ansible
description: Allow HTTP and SSH traffic
rules:
- proto: tcp
ports: [8080]
cidr_ip: 0.0.0.0/0
- proto: tcp
ports: [22]
cidr_ip: 0.0.0.0/0
register: aws_security_group
- name: Create EC2 key pair
amazon.aws.ec2_key:
name: ansible-ch2
file_name: ansible-ch2.key
no_log: true
register: aws_ec2_key_pair
- name: Create multiple EC2 instances
amazon.aws.ec2_instance:
name: "sample-app-ansible-{{ item }}" # Noms uniques : sample-app-ansible-0, -1, -2
key_name: "{{ aws_ec2_key_pair.key.name }}"
instance_type: "{{ instance_type }}"
security_group: "{{ aws_security_group.group_id }}"
image_id: "{{ image_id }}"
wait: yes
wait_timeout: 500
tags:
Ansible: ch2_instances
Index: "{{ item }}" # Tag pour identifier chaque instance
register: ec2_instances
loop: "{{ range(0, instance_count) | list }}" # Boucle de 0 à instance_count-1
- name: Display instance information
debug:
msg: "Instance {{ item.instances[0].instance_id }} created with IP {{ item.instances[0].public_ip_address }}"
loop: "{{ ec2_instances.results }}"Modifications clés :
instance_count: 3— variable contrôlable.loop: "{{ range(0, instance_count) | list }}"— crée N instances.name: "sample-app-ansible-{{ item }}"— noms uniques.tags: { Ansible: ch2_instances, Index: "{{ item }}" }— identifier chaque instance.
Approche 2 : Inventaire dynamique (découverte automatique)
L’inventaire inventory.aws_ec2.yml fonctionne déjà !
Grâce au filtre tag:Ansible: ch2_instances, Ansible découvre automatiquement TOUTES les instances tagguées, peu importe le nombre.
plugin: amazon.aws.aws_ec2
regions:
- us-east-2
filters:
instance-state-name: running
tag:Ansible: ch2_instances # Découvre tous les instances avec ce tag
keyed_groups:
- key: tags.Ansible
leading_separator: ''
compose:
ansible_host: public_ip_addressApproche 3 : Lancer la configuration sur toutes les instances
Le playbook configure_sample_app_playbook.yml cible déjà _ch2_instances.
Grâce à l’inventaire dynamique, il s’exécute automatiquement sur TOUTES les instances découvertes :
- name: Configure the EC2 instance to run a sample app
hosts: _ch2_instances # Cible tous les hôtes du groupe dynamique
gather_facts: true
become: true
roles:
- sample-appRésumé du workflow multi-instance
-
Créer les instances (avec le playbook modifié)
ANSIBLE_PYTHON_INTERPRETER="$(which python3)" \ AWS_PROFILE=labs-devops_diallo \ ansible-playbook -v create_ec2_instance_playbook.yml \ -e instance_count=3 \ -e instance_type=t3.micro -
L’inventaire découvre automatiquement les 3 instances (plugin aws_ec2 + filtre tag).
-
Configurer toutes les instances en parallèle
ANSIBLE_PYTHON_INTERPRETER="$(which python3)" \ AWS_PROFILE=labs-devops_diallo \ ansible-playbook -i inventory.aws_ec2.yml -v configure_sample_app_playbook.ymlAnsible se connecte à toutes les 3 instances en parallèle et exécute le rôle.
-
Vérifier les 3 apps
for i in {1..3}; do # Récupérer l'IP de chaque instance (à adapter selon l'output) echo "Instance $i : $(curl -s http://INSTANCE_IP:8080/)" done
Avantages
✅ Pas de changement de code nécessaire pour 2, 3 ou 10 instances.
✅ L’inventaire dynamique s’adapte automatiquement.
✅ Parallélisation automatique par Ansible.
✅ Même configuration appliquée à toutes les instances.
Limitations
⚠️ Les instances partagent le même Security Group et key pair — adapter si isolation nécessaire.
⚠️ Chaque instance obtient le même Name=sample-app-ansible-* — utiliser les tags Index ou numéro pour distinguer.
Conclusion
-
Exercise 3 : L’idempotence révèle les tâches mal écrites (comme “Start sample app” qui relance toujours l’app). Une bonne pratique : utiliser
changed_when: false,creates:, oustate: presentpour signaler les tâches idempotentes. -
Exercise 4 : Grâce au plugin
aws_ec2et aux tags, déployer N instances nécessite juste une modification de variable (instance_count) — l’inventaire et la configuration se font automatiquement.