Test-Driven Infrastructure : Simuler AWS en local pour vos tests d'infrastructure
Introduction
Le développement piloté par les tests (TDD) n'est plus réservé au code applicatif. L'Infrastructure as Code (IaC) bénéficie également de cette approche, permettant de valider nos configurations avant tout déploiement en production. Mais comment tester efficacement sans provisionner de vraies ressources AWS coûteuses ?
Dans cet article, nous allons explorer plusieurs solutions pour simuler un environnement AWS en local, puis déployer Apache sur une VM virtuelle avec Ansible, le tout dans une approche test-driven.
Pourquoi simuler AWS en local ?
Avantages de la simulation
- Réduction des coûts : Pas de frais AWS pendant les phases de développement et test
- Rapidité d'exécution : Les tests s'exécutent en quelques secondes au lieu de minutes
- Isolation complète : Aucun risque d'impacter des ressources de production
- Reproductibilité : Environnement identique pour tous les développeurs
- CI/CD simplifié : Tests exécutables dans les pipelines sans credentials AWS
Les outils de simulation AWS
1. LocalStack : L'émulateur AWS complet
LocalStack est la solution la plus populaire pour émuler les services AWS localement. Il supporte plus de 80 services AWS.
Installation avec Docker :
# Démarrer LocalStack
docker run --rm -it \
-p 4566:4566 \
-p 4571:4571 \
-v /var/run/docker.sock:/var/run/docker.sock \
localstack/localstack
Configuration AWS CLI pour LocalStack :
# Configurer l'endpoint local
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1
# Créer une instance EC2 sur LocalStack
aws ec2 run-instances \
--image-id ami-12345678 \
--count 1 \
--instance-type t2.micro \
--endpoint-url http://localhost:4566
2. Moto : Mock AWS Services en Python
Moto est une bibliothèque Python permettant de mocker les services AWS, idéale pour les tests unitaires.
import boto3
from moto import mock_ec2
@mock_ec2
def test_create_instance():
ec2 = boto3.resource('ec2', region_name='us-east-1')
# Créer une instance mockée
instances = ec2.create_instances(
ImageId='ami-12345',
MinCount=1,
MaxCount=1,
InstanceType='t2.micro'
)
assert len(instances) == 1
assert instances[0].instance_type == 't2.micro'
3. Vagrant + VirtualBox : Simulation de VM complète
Pour un contrôle total sur l'environnement de test, Vagrant reste une excellente option.
Vagrantfile exemple :
Vagrant.configure("2") do |config|
# Configuration de la VM "AWS-like"
config.vm.define "aws-vm" do |vm|
vm.vm.box = "ubuntu/focal64"
vm.vm.hostname = "aws-instance"
# Configuration réseau
vm.vm.network "private_network", ip: "192.168.56.10"
# Ressources
vm.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
vb.cpus = 2
vb.name = "aws-test-instance"
end
# Provisioning initial
vm.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y python3-pip
pip3 install ansible
SHELL
end
end
Cas pratique : Déploiement d'Apache avec Ansible
Architecture du projet
project/
├── inventory/
│ ├── local/
│ │ └── hosts.yml
│ └── production/
│ └── hosts.yml
├── playbooks/
│ └── deploy-apache.yml
├── roles/
│ └── apache/
│ ├── tasks/
│ │ └── main.yml
│ ├── templates/
│ │ └── index.html.j2
│ ├── handlers/
│ │ └── main.yml
│ └── defaults/
│ └── main.yml
├── tests/
│ ├── test_infrastructure.py
│ └── molecule/
│ └── default/
│ ├── molecule.yml
│ └── verify.yml
├── Vagrantfile
└── requirements.txt
1. Configuration de l'inventaire Ansible
inventory/local/hosts.yml :
all:
hosts:
aws-vm:
ansible_host: 192.168.56.10
ansible_user: vagrant
ansible_ssh_private_key_file: .vagrant/machines/aws-vm/virtualbox/private_key
ansible_python_interpreter: /usr/bin/python3
vars:
environment: local
apache_port: 80
2. Rôle Ansible pour Apache
roles/apache/tasks/main.yml :
---
- name: Installer Apache
apt:
name: apache2
state: present
update_cache: yes
become: yes
tags:
- apache
- install
- name: Créer le répertoire de configuration custom
file:
path: /etc/apache2/sites-available/custom
state: directory
mode: '0755'
become: yes
- name: Configurer le port d'écoute
lineinfile:
path: /etc/apache2/ports.conf
regexp: '^Listen '
line: "Listen {{ apache_port }}"
become: yes
notify: restart apache
- name: Déployer la page d'accueil
template:
src: index.html.j2
dest: /var/www/html/index.html
mode: '0644'
become: yes
tags:
- deploy
- name: S'assurer qu'Apache est démarré et activé
systemd:
name: apache2
state: started
enabled: yes
become: yes
roles/apache/templates/index.html.j2 :
<!DOCTYPE html>
<html>
<head>
<title>Test Infrastructure AWS</title>
</head>
<body>
<h1>Bienvenue sur {{ ansible_hostname }}</h1>
<p>Environnement : {{ environment }}</p>
<p>Déployé le : {{ ansible_date_time.iso8601 }}</p>
<p>Apache écoute sur le port : {{ apache_port }}</p>
</body>
</html>
roles/apache/handlers/main.yml :
3. Playbook principal
playbooks/deploy-apache.yml :
---
- name: Déployer Apache sur les instances
hosts: all
gather_facts: yes
pre_tasks:
- name: Afficher les informations de l'hôte
debug:
msg: "Déploiement sur {{ inventory_hostname }} ({{ ansible_host }})"
- name: Vérifier la connectivité
ping:
roles:
- role: apache
tags: apache
post_tasks:
- name: Vérifier que le service répond
uri:
url: "http://{{ ansible_host }}:{{ apache_port }}"
status_code: 200
delegate_to: localhost
tags: verify
4. Tests avec Molecule
Molecule permet de tester les rôles Ansible de manière automatisée.
Installation :
tests/molecule/default/molecule.yml :
---
dependency:
name: galaxy
driver:
name: vagrant
provider:
name: virtualbox
platforms:
- name: test-instance
box: ubuntu/focal64
memory: 1024
cpus: 2
groups:
- webservers
provisioner:
name: ansible
playbooks:
converge: ../../../playbooks/deploy-apache.yml
inventory:
host_vars:
test-instance:
apache_port: 8080
environment: test
verifier:
name: ansible
tests/molecule/default/verify.yml :
---
- name: Vérifier le déploiement Apache
hosts: all
gather_facts: no
tasks:
- name: Vérifier qu'Apache est installé
package:
name: apache2
state: present
check_mode: yes
register: apache_installed
failed_when: apache_installed.changed
- name: Vérifier qu'Apache écoute sur le bon port
wait_for:
port: "{{ apache_port | default(80) }}"
host: "{{ ansible_host }}"
state: started
timeout: 30
- name: Tester la page d'accueil
uri:
url: "http://{{ ansible_host }}:{{ apache_port | default(80) }}"
return_content: yes
register: response
- name: Vérifier le contenu de la page
assert:
that:
- response.status == 200
- "'Bienvenue' in response.content"
- "'Apache écoute sur le port' in response.content"
5. Tests Python avec pytest
tests/test_infrastructure.py :
import pytest
import requests
import subprocess
import time
from paramiko import SSHClient, AutoAddPolicy
class TestInfrastructure:
@classmethod
def setup_class(cls):
"""Démarrer la VM Vagrant"""
subprocess.run(["vagrant", "up"], check=True)
time.sleep(10) # Attendre que la VM soit prête
@classmethod
def teardown_class(cls):
"""Arrêter la VM Vagrant"""
subprocess.run(["vagrant", "halt"], check=False)
def test_vm_is_running(self):
"""Vérifier que la VM est accessible"""
result = subprocess.run(
["vagrant", "status"],
capture_output=True,
text=True
)
assert "running" in result.stdout
def test_ssh_connectivity(self):
"""Tester la connexion SSH"""
ssh = SSHClient()
ssh.set_missing_host_key_policy(AutoAddPolicy())
# Récupérer les infos SSH de Vagrant
result = subprocess.run(
["vagrant", "ssh-config"],
capture_output=True,
text=True
)
# Parser la config (simplifié)
ssh.connect(
hostname="127.0.0.1",
port=2222,
username="vagrant",
key_filename=".vagrant/machines/aws-vm/virtualbox/private_key"
)
stdin, stdout, stderr = ssh.exec_command("hostname")
assert stdout.read().decode().strip() == "aws-instance"
ssh.close()
def test_ansible_deployment(self):
"""Exécuter le playbook Ansible"""
result = subprocess.run(
[
"ansible-playbook",
"-i", "inventory/local/hosts.yml",
"playbooks/deploy-apache.yml"
],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "failed=0" in result.stdout
def test_apache_is_running(self):
"""Vérifier qu'Apache répond"""
response = requests.get("http://192.168.56.10")
assert response.status_code == 200
assert "Bienvenue" in response.text
@pytest.mark.parametrize("endpoint,expected_status", [
("/", 200),
("/nonexistent", 404),
])
def test_apache_endpoints(self, endpoint, expected_status):
"""Tester différents endpoints"""
response = requests.get(f"http://192.168.56.10{endpoint}")
assert response.status_code == expected_status
Pipeline CI/CD avec GitHub Actions
.github/workflows/test-infrastructure.yml :
name: Test Infrastructure
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Installer les dépendances
run: |
pip install ansible-lint yamllint
- name: Lint Ansible
run: |
ansible-lint playbooks/
yamllint inventory/
test-local:
runs-on: ubuntu-latest
services:
localstack:
image: localstack/localstack
ports:
- 4566:4566
env:
SERVICES: ec2,s3
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Installer les dépendances
run: |
pip install -r requirements.txt
pip install boto3 moto pytest
- name: Tester avec Moto
run: |
pytest tests/test_aws_mocks.py
- name: Tester avec LocalStack
run: |
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1
# Créer une instance dans LocalStack
aws ec2 run-instances \
--image-id ami-12345678 \
--instance-type t2.micro \
--endpoint-url http://localhost:4566
molecule-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Installer Molecule
run: |
pip install molecule[docker] ansible
- name: Exécuter les tests Molecule
run: |
cd roles/apache
molecule test
Bonnes pratiques et recommandations
1. Stratégie de test en pyramide
- Tests unitaires (base) : Mocker avec Moto pour valider la logique
- Tests d'intégration (milieu) : LocalStack pour tester les interactions
- Tests end-to-end (sommet) : Vagrant/Docker pour validation complète
2. Configuration par environnement
# group_vars/all.yml
apache_defaults:
port: 80
max_clients: 150
# group_vars/local.yml
apache_port: 8080
debug_mode: true
# group_vars/production.yml
apache_port: 443
ssl_enabled: true
3. Gestion des secrets
# Utiliser Ansible Vault pour les secrets
ansible-vault create secrets.yml
ansible-playbook deploy.yml --ask-vault-pass
4. Monitoring et validation
- name: Validation post-déploiement
block:
- name: Vérifier les métriques système
shell: |
free -m
df -h
systemctl status apache2
register: system_check
- name: Test de charge simple
uri:
url: "http://{{ ansible_host }}"
method: GET
loop: "{{ range(1, 10) | list }}"
loop_control:
pause: 0.5
Comparaison des solutions
Solution | Avantages | Inconvénients | Cas d'usage |
---|---|---|---|
LocalStack | Émulation complète AWS, Nombreux services | Consomme des ressources, Différences avec AWS réel | Tests d'intégration complets |
Moto | Léger, Intégration Python native | Limité à Python, Mock partiel | Tests unitaires Python |
Vagrant | VM complète, Proche de la production | Lent à démarrer, Consomme beaucoup de ressources | Tests end-to-end |
Docker | Rapide, Léger | Pas de VM complète, Limitations réseau | Tests de conteneurs |
Molecule | Framework de test complet, CI/CD ready | Courbe d'apprentissage | Tests de rôles Ansible |
Conclusion
Le test-driven infrastructure n'est plus un luxe mais une nécessité pour garantir la fiabilité de nos déploiements. En combinant des outils comme LocalStack, Vagrant et Ansible avec une stratégie de tests bien pensée, nous pouvons :
- Développer notre infrastructure avec confiance
- Réduire les coûts de développement
- Accélérer les cycles de déploiement
- Maintenir une qualité constante
La clé du succès réside dans le choix des bons outils pour chaque niveau de test et l'automatisation complète du processus via CI/CD.
Ressources complémentaires
- LocalStack Documentation
- Moto Documentation
- Molecule Documentation
- Ansible Best Practices
- Vagrant Documentation
Cet article vous a été utile ? N'hésitez pas à partager vos retours d'expérience sur l'infrastructure as code et vos stratégies de test !