87 % des incidents cloud majeurs sont causés par des erreurs de configuration humaine (Gartner). Pas par des attaques, pas par des pannes matérielles — par des humains qui cliquent dans la mauvaise console, qui oublient un paramètre, qui configurent un serveur différemment du précédent. En environnement réglementé (banque, télécoms, secteur public), ces erreurs ne sont pas seulement des incidents : ce sont des non-conformités.
L’Infrastructure as Code (IaC) transforme chaque modification d’infrastructure en un acte intentionnel, versionné et auditable. Terraform provisionne les ressources, Ansible les configure, Git garde la trace, et le DSI dort la nuit.
Ce guide vous montre comment déployer une stack IaC complète sur votre cloud privé, en conformité avec NIS2 et DORA — sans vendor lock-in.
Pourquoi l’IaC n’est plus optionnel en 2026
Le coût de la configuration manuelle
| Coût | Configuration manuelle | Infrastructure as Code |
|---|---|---|
| Provisionnement | 2-4 h par serveur | 5 min (terraform apply) |
| Configuration | 1-2 h par service | 10 min (ansible-playbook) |
| Reproductibilité | Nulle (snowflake servers) | Totale (git checkout + apply) |
| Audit trail | Difficile (qui a cliqué quoi ?) | Natif (git log) |
| Rollback | Heures de recherche | 1 commande (terraform rollback) |
| Dérive de configuration | Inévitable | Détectée et corrigée automatiquement |
Pour un DSI qui gère 50+ serveurs, la différence entre l’IaC et le manuel n’est pas un confort — c’est 30 heures par mois de gain opérationnel et zéro dérive.
NIS2 et DORA exigent la traçabilité
NIS2 (Art. 21) exige des « procédures de gestion des changements ». DORA exige que chaque modification sur un système ICT financier soit documentée, approuvée et traçable. L’IaC est la seule méthode qui satisfait ces exigences nativement :
- Qui a fait le changement ? → Git commit author
- Quoi a changé ? → Git diff
- Quand ? → Git timestamp
- Pourquoi ? → Git commit message + référence au ticket
- Où ? → Code Terraform/Ansible avec la déclaration complète
L’architecture IaC Cloud Inspire
Vue d’ensemble
┌──────────────────────────────────────────────────────────────┐
│ GITLAB (Git Repository) │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Terraform│ │ Ansible │ │ Values │ │ Pipelines │ │
│ │ (.tf) │ │(.yml) │ │ (.yaml) │ │ CI/CD │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
└───────┼──────────────┼──────────────┼──────────────┼──────────┘
│ │ │ │
▼ ▼ │ ▼
┌───────────────┐ ┌──────────┐ │ ┌──────────────────┐
│ Terraform │ │ Ansible │ │ │ GitLab CI/CD │
│ Cloud Provider│ │ Config │ │ │ (validation + │
│ (OpenNebula) │ │ Manager │ │ │ plan → apply) │
└───────┬───────┘ └────┬─────┘ │ └──────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ CLOUD PRIVÉ (OpenNebula) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────────┐ │
│ │ VMs │ │ Réseaux│ │ Stockage│ │ Services │ │
│ │ │ │ (VNet) │ │ (Data) │ │ (App) │ │
│ └────────┘ └────────┘ └────────┘ └────────────┘ │
└──────────────────────────────────────────────────────┘
Séparation des responsabilités
| Outil | Rôle | Portée | Fréquence de changement |
|---|---|---|---|
| Terraform | Provisionnement | VM, réseaux, stockage, firewalls | Faible (semaines/mois) |
| Ansible | Configuration | Packages, utilisateurs, services, sécurité | Moyenne (jours/semaines) |
| Git | Versionnage | Tout l’état désiré | Continue |
| GitLab CI/CD | Validation | Tests, plans, déploiement | À chaque commit |
Cette séparation est fondamentale : Terraform gère la couche ressource (ce qui existe), Ansible gère la couche service (comment c’est configuré), et Git gère la couche intention (ce qui est voulu).
Terraform : provisionner sans surprise
Configuration de base pour OpenNebula
# main.tf — Provisionnement cloud privé OpenNebula
terraform {
required_version = ">= 1.7"
required_providers {
opennebula = {
source = "OpenNebula/opennebula"
version = "~> 1.4"
}
}
backend "s3" {
bucket = "terraform-state-cloud-inspire"
key = "production/terraform.tfstate"
region = "eu-west-1"
}
}
provider "opennebula" {
endpoint = var.one_endpoint
username = var.one_user
password = var.one_password
}
# Variables structurées
variable "environment" {
description = "Environnement cible (staging/production)"
type = string
default = "production"
}
variable "vm_spec" {
description = "Spécifications des VMs par rôle"
type = map(object({
vcpu = number
memory = number # en MB
disk = number # en GB
count = number
role = string
}))
default = {
web = { vcpu = 2, memory = 4096, disk = 40, count = 2, role = "web" }
api = { vcpu = 4, memory = 8192, disk = 80, count = 2, role = "api" }
db = { vcpu = 4, memory = 16384, disk = 200, count = 1, role = "database" }
}
}
Module de VM avec sécurité intégrée
# modules/vm/main.tf — Module VM avec sécurité de base
resource "opennebula_virtual_machine" "vm" {
for_each = var.instances
name = "${var.project}-${var.environment}-${each.value.role}-${format("%02d", each.key + 1)}"
description = "VM ${each.value.role} — ${var.project} ${var.environment}"
cpu = each.value.vcpu
vcpu = each.value.vcpu
memory = each.value.memory
context = {
DNS = "1.1.1.1,8.8.8.8"
NETWORK = "YES"
SSH_KEY = var.ssh_public_key
USERDATA = data.cloud_init_config.init.cloud_config
}
disk {
image_id = var.base_image_id
size = each.value.disk
}
nic {
network_id = var.network_id
}
tags = {
Environment = var.environment
Role = each.value.role
ManagedBy = "terraform"
Compliance = "NIS2-DORA"
Project = var.project
}
}
# Cloud-init pour configuration de base (durcissement)
data "cloud_init_config" "init" {
gzip = true
base64_encode = true
part {
content_type = "text/cloud-config"
content = yamlencode({
package_update = true
package_upgrade = true
packages = ["fail2ban", "unattended-upgrades", "auditd"]
users = [{
name = "admin"
groups = ["sudo"]
sudo = "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys = [var.ssh_public_key]
}]
write_files = [{
path = "/etc/security/limits.d/hardening.conf"
content = <<-EOT
* soft nofile 65536
* hard nofile 65536
* soft nproc 4096
* hard nproc 4096
EOT
}]
runcmd = [
"systemctl enable fail2ban",
"systemctl start fail2ban",
"systemctl enable auditd",
"systemctl start auditd"
]
})
}
}
Gestion des environnements
# environments/production.tfvars
environment = "production"
project = "cloud-inspire"
vm_spec = {
web = { vcpu = 4, memory = 8192, disk = 60, count = 3, role = "web" }
api = { vcpu = 8, memory = 16384, disk = 100, count = 3, role = "api" }
database = { vcpu = 8, memory = 32768, disk = 500, count = 2, role = "database" }
monitoring = { vcpu = 4, memory = 8192, disk = 80, count = 1, role = "monitoring" }
bastion = { vcpu = 2, memory = 2048, disk = 20, count = 1, role = "bastion" }
}
# environments/staging.tfvars
environment = "staging"
project = "cloud-inspire"
vm_spec = {
web = { vcpu = 2, memory = 4096, disk = 40, count = 1, role = "web" }
api = { vcpu = 2, memory = 4096, disk = 60, count = 1, role = "api" }
database = { vcpu = 4, memory = 16384, disk = 200, count = 1, role = "database" }
monitoring = { vcpu = 2, memory = 4096, disk = 60, count = 1, role = "monitoring" }
}
Les 4 commandes Terraform du DSI
| Commande | Usage | Fréquence |
|---|---|---|
terraform plan | Voir ce qui va changer avant de l’appliquer | À chaque modification |
terraform apply | Appliquer les changements | Après validation du plan |
terraform output | Voir les sorties (IPs, IDs) | Pour les playbooks Ansible |
terraform state rm | Retirer une ressource du state sans la détruire | Rare (dépannage) |
Règle d’or : jamais de terraform apply sans un terraform plan préalable revu par un pair. C’est la base du GitOps.
Ansible : configurer sans dérive
Structure du projet
ansible/
├── inventory/
│ ├── production.yml # Inventaire production
│ └── staging.yml # Inventaire staging
├── group_vars/
│ ├── all.yml # Variables globales
│ ├── web.yml # Variables serveurs web
│ ├── api.yml # Variables serveurs API
│ └── database.yml # Variables base de données
├── playbooks/
│ ├── site.yml # Playbook principal (tout configurer)
│ ├── web.yml # Configuration serveurs web
│ ├── api.yml # Configuration serveurs API
│ ├── database.yml # Configuration base de données
│ ├── monitoring.yml # Installation Grafana/Prometheus/Loki
│ └── hardening.yml # Durcissement sécurité NIS2
├── roles/
│ ├── base/ # Rôle de base (commun à tous)
│ ├── nginx/ # Configuration Nginx
│ ├── docker/ # Installation Docker
│ ├── security/ # Durcissement OS
│ ├── monitoring/ # Stack observabilité
│ └── backup/ # Backup automatisé
└── ansible.cfg # Configuration Ansible
Playbook de durcissement NIS2
# playbooks/hardening.yml — Durcissement conforme NIS2
- name: "Durcissement NIS2 — Configuration de sécurité de base"
hosts: all
become: true
roles:
- base
- security
- name: "Durcissement réseau — Firewall et segmentation"
hosts: all
become: true
tasks:
- name: Installer UFW
apt:
name: ufw
state: present
- name: Configurer les règles UFW (par défaut deny incoming)
ufw:
policy: deny
direction: incoming
- name: Autoriser SSH avec rate limiting
ufw:
rule: limit
port: "{{ ssh_port | default(22) }}"
proto: tcp
- name: Autoriser HTTP/HTTPS sur les serveurs web
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- 80
- 443
when: "'web' in group_names"
- name: Activer UFW
ufw:
state: enabled
- name: "Audit NIS2 — Journalisation et traçabilité"
hosts: all
become: true
tasks:
- name: Installer auditd
apt:
name: auditd
state: present
- name: Configurer les règles d'audit
copy:
dest: /etc/audit/rules.d/cloud-inspire.rules
content: |
# Surveillance des modifications de fichiers critiques
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/ssh/sshd_config -p wa -k ssh
# Surveillance des commandes privilégiées
-a always,exit -F arch=b64 -S execve -F uid=0 -k privileged
# Surveillance des changements réseau
-a always,exit -F arch=b64 -S sethostname -k network
# Surveillance des changements système
-w /etc/sudoers -p wa -k sudoers
-w /etc/cron* -p wa -k cron
notify: restart auditd
handlers:
- name: restart auditd
service:
name: auditd
state: restarted
Playbook d’application (serveur web)
# playbooks/web.yml — Configuration serveur web avec Nginx + TLS
- name: "Configuration serveurs web"
hosts: web
become: true
roles:
- nginx
tasks:
- name: Installer les packages web
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
state: present
update_cache: true
- name: Déployer la configuration Nginx
template:
src: templates/nginx-site.conf.j2
dest: /etc/nginx/sites-available/{{ project }}
owner: root
group: root
mode: '0644'
notify: reload nginx
- name: Activer le site Nginx
file:
src: /etc/nginx/sites-available/{{ project }}
dest: /etc/nginx/sites-enabled/{{ project }}
state: link
notify: reload nginx
- name: Vérifier la configuration Nginx
command: nginx -t
changed_when: false
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
Idempotence : configurez 100 fois, le résultat est le même
La force d’Ansible contre NIS2 et DORA : l’idempotence. Un playbook Ansible exécuté 100 fois donne exactement le même résultat qu’exécuté 1 fois. Pas de dérive, pas de surprise.
Si un administrateur modifie manuellement un fichier de configuration, le prochain passage d’Ansible le remet dans l’état désiré. C’est la convergence continue — le contraire du snowflake server.
GitOps : le pipeline CI/CD de l’infrastructure
Pipeline GitLab pour Terraform
# .gitlab-ci.yml — Pipeline IaC
stages:
- validate
- plan
- apply
- configure
variables:
TF_ROOT: "${CI_PROJECT_DIR}/terraform"
ANSIBLE_ROOT: "${CI_PROJECT_DIR}/ansible"
# Validation syntaxique
terraform:validate:
stage: validate
image: hashicorp/terraform:1.7
script:
- terraform init -backend=false
- terraform validate
- terraform fmt -check
rules:
- changes:
- "terraform/**/*"
# Plan — voir ce qui va changer
terraform:plan:
stage: plan
image: hashicorp/terraform:1.7
script:
- terraform init
- terraform plan -out=plan.tfplan
- terraform show -no-color plan.tfplan > plan.txt
artifacts:
paths:
- plan.tfplan
- plan.txt
rules:
- changes:
- "terraform/**/*"
# Apply — appliquer les changements (manual gate)
terraform:apply:
stage: apply
image: hashicorp/terraform:1.7
script:
- terraform init
- terraform apply plan.tfplan
when: manual # ← Exige une approbation manuelle (NIS2 change management)
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- "terraform/**/*"
# Configuration avec Ansible
ansible:configure:
stage: configure
image: ansibles/ansible:latest
script:
- ansible-playbook -i inventory/production.yml playbooks/site.yml
rules:
- changes:
- "ansible/**/*"
Les 4 gates du pipeline IaC
| Gate | Outil | Objectif NIS2/DORA |
|---|---|---|
| Validation | terraform validate | Cohérence syntaxique |
| Plan | terraform plan | Revue des changements avant application |
| Approval | when: manual | Approbation obligatoire (change management) |
| Audit | Git log + artifacts | Traçabilité complète |
Résultat : chaque changement d’infrastructure suit le même circuit qu’un changement de code applicatif. C’est exactement ce que DORA exige pour la résilience opérationnelle des systèmes ICT.
Conformité réglementaire
NIS2 : gestion des changements et traçabilité
| Exigence NIS2 | Réponse IaC |
|---|---|
| Gestion des changements (Art. 21.2.d) | Terraform plan + approval gate GitLab |
| Traçabilité des modifications | Git log (qui, quoi, quand, pourquoi) |
| Politique de sécurité des systèmes | Ansible hardening (playbook reproductible) |
| Gestion des vulnérabilités | unattended-upgrades + Ansible patching |
| Continuité d’activité | Infrastructure reproductible en 30 min via terraform apply |
DORA : résilience opérationnelle ICT
| Exigence DORA | Réponse IaC |
|---|---|
| Tests de résilience | Détruire et recréer un environnement en 30 min |
| Gestion des changements ICT | GitOps pipeline avec gates d’approbation |
| Documentation des systèmes | Terraform = documentation vivante |
| Gestion des incidents | Rollback en 1 commande (git revert + terraform apply) |
| Troisième lieu de reprise | Staging identique = clone Terraform de prod |
RGPD : sécurité des traitements
L’article 32 exige des « mesures techniques appropriées » :
- Intégrité : Ansible garantit la configuration souhaitée (idempotence)
- Confidentialité : Secrets dans Vault, pas en clair dans Git
- Disponibilité : Infrastructure reproductible = reprise rapide
- Traçabilité : Git log = registre de toutes les modifications
Secrets Management : ne jamais committer un secret
Terraform + HashiCorp Vault
# Vault pour les secrets — Pas de secret en clair dans Git
data "vault_generic_secret" "db_credentials" {
path = "secret/data/database/production"
}
resource "opennebula_virtual_machine" "database" {
# ... configuration VM ...
context = {
DB_USER = data.vault_generic_secret.db_credentials.data["username"]
DB_PASS = data.vault_generic_secret.db_credentials.data["password"]
}
}
Ansible Vault pour les variables sensibles
# Chiffrer les variables sensibles
ansible-vault encrypt group_vars/production/vault.yml
# Utiliser dans un playbook
ansible-playbook playbooks/site.yml --ask-vault-password
# Les variables sensibles sont chiffrées dans Git
# Seules les personnes avec le mot de passe Vault peuvent les lire
Règle absolue : aucun secret en clair dans Git. Pas d’exception. Terraform + Vault pour les secrets infrastructure, Ansible Vault pour les secrets configuration.
Monitoring de l’IaC : Terraform et Ansible dans Grafana
Dashboards essentiels
| Dashboard | Métriques | Alerte |
|---|---|---|
| Terraform State | Drift detection, plan changes, apply success | Drift détecté → Slack |
| Ansible Runs | Playbook duration, task failures, changed hosts | Failure > 0 → escalade |
| Configuration Drift | Écart entre état désiré et réel | Drift > 5 % → investigation |
| Security Compliance | Packages patched, firewall rules, audit rules | CVE non patchée > 48h → critique |
Alerting IaC
# Alertes Grafana pour la conformité IaC
groups:
- name: iac_compliance
rules:
# Drift Terraform — quelqu'un a modifié l'infrastructure manuellement
- alert: TerraformDriftDetected
expr: terraform_drift_resources > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Configuration drift détecté sur {{ $labels.workspace }}"
# Playbook Ansible en échec
- alert: AnsiblePlaybookFailed
expr: ansible_playbook_failures_total > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Playbook {{ $labels.playbook }} en échec"
# Serveur non conforme (dernier run > 24h)
- alert: HostNotConverged
expr: ansible_last_run_timestamp < (time() - 86400)
for: 1h
labels:
severity: warning
annotations:
summary: "{{ $labels.host }} non convergé depuis plus de 24h"
Le coût du non-IaC : calcul du ROI
Scénario : 30 serveurs, 5 environnements
| Item | Sans IaC | Avec IaC | Économie |
|---|---|---|---|
| Provisionnement initial | 120 h (4h/serveur) | 2 h (terraform apply) | 118 h |
| Configuration initiale | 60 h (2h/serveur) | 1 h (ansible-playbook) | 59 h |
| Patching mensuel | 30 h (1h/serveur) | 15 min (1 playbook) | 29.75 h |
| Reprovisionnement DR | 120 h | 2 h | 118 h |
| Audit de configuration | 40 h (manuel) | 5 min (terraform plan + ansible —check) | 39.9 h |
| Total annuel | 2 340 h | 40 h | 2 300 h |
À 75 €/h (taux DSI), l’IaC économise 172 500 €/an sur 30 serveurs. L’investissement initial (formation + mise en place) est amorti en moins de 3 mois.
Déploiement en 5 jours
| Jour | Action | Livrable |
|---|---|---|
| J1 | Installer Terraform + Ansible + Git | Environnement IaC opérationnel |
| J2 | Modules Terraform (VM, réseau, stockage) | Infrastructure provisionnée par code |
| J3 | Playbooks Ansible (hardening, services) | Configuration automatisée et idempotente |
| J4 | Pipeline GitLab CI/CD + Vault | GitOps avec approval gates |
| J5 | Dashboards Grafana + alertes | Monitoring IaC et drift detection |
Prérequis : GitLab accès admin, OpenNebula accès API, Vault instance déployée.
Conclusion
L’Infrastructure as Code n’est pas une option technique — c’est une obligation réglementaire en environnement NIS2/DORA. Chaque modification manuelle est un risque de non-conformité. Chaque snowflake server est un incident en devenir. Chaque procédure documentée mais non exécutée automatiquement est une dérive assumée.
Terraform + Ansible + Git = infrastructure reproductible, traçable et conforme. Terraform provisionne, Ansible configure, Git trace. Le DSI a une vue complète de l’état de son infrastructure, peut recréer n’importe quel environnement en 30 minutes, et satisfait nativement les exigences de changement et de traçabilité de NIS2 et DORA.
Cloud Inspire déploie la stack IaC complète en 5 jours sur votre cloud privé OpenNebula, avec les modules Terraform, les playbooks Ansible, le pipeline GitLab et les dashboards Grafana préconfigurés. Si vous voulez arrêter de configurer vos serveurs à la main et commencer à les coder, parlons-en.
FAQ
- Quelle est la différence entre Terraform et Ansible ?
- L’IaC est-elle obligatoire pour la conformité NIS2 ?
- Combien de temps faut-il pour adopter l’IaC sur une infrastructure existante ?
- Comment gérer les secrets avec Terraform et Ansible ?
- Peut-on utiliser Terraform avec d’autres clouds qu’OpenNebula ?
- Qu’est-ce que le drift detection et pourquoi est-ce important ?