Passer au contenu principal

Infrastructure as Code Terraform + Ansible : le guide du DSI pour un cloud reproductible et conforme

Par Cloud Inspire · 14 mai 2026 · 1 min de lecture

TerraformAnsibleInfrastructure as CodeIaCGitOpsDSIFrench

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ûtConfiguration manuelleInfrastructure as Code
Provisionnement2-4 h par serveur5 min (terraform apply)
Configuration1-2 h par service10 min (ansible-playbook)
ReproductibilitéNulle (snowflake servers)Totale (git checkout + apply)
Audit trailDifficile (qui a cliqué quoi ?)Natif (git log)
RollbackHeures de recherche1 commande (terraform rollback)
Dérive de configurationInévitableDé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 :


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

OutilRôlePortéeFréquence de changement
TerraformProvisionnementVM, réseaux, stockage, firewallsFaible (semaines/mois)
AnsibleConfigurationPackages, utilisateurs, services, sécuritéMoyenne (jours/semaines)
GitVersionnageTout l’état désiréContinue
GitLab CI/CDValidationTests, 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

CommandeUsageFréquence
terraform planVoir ce qui va changer avant de l’appliquerÀ chaque modification
terraform applyAppliquer les changementsAprès validation du plan
terraform outputVoir les sorties (IPs, IDs)Pour les playbooks Ansible
terraform state rmRetirer une ressource du state sans la détruireRare (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

GateOutilObjectif NIS2/DORA
Validationterraform validateCohérence syntaxique
Planterraform planRevue des changements avant application
Approvalwhen: manualApprobation obligatoire (change management)
AuditGit log + artifactsTraç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 NIS2Réponse IaC
Gestion des changements (Art. 21.2.d)Terraform plan + approval gate GitLab
Traçabilité des modificationsGit log (qui, quoi, quand, pourquoi)
Politique de sécurité des systèmesAnsible hardening (playbook reproductible)
Gestion des vulnérabilitésunattended-upgrades + Ansible patching
Continuité d’activitéInfrastructure reproductible en 30 min via terraform apply

DORA : résilience opérationnelle ICT

Exigence DORARéponse IaC
Tests de résilienceDétruire et recréer un environnement en 30 min
Gestion des changements ICTGitOps pipeline avec gates d’approbation
Documentation des systèmesTerraform = documentation vivante
Gestion des incidentsRollback en 1 commande (git revert + terraform apply)
Troisième lieu de repriseStaging identique = clone Terraform de prod

RGPD : sécurité des traitements

L’article 32 exige des « mesures techniques appropriées » :


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

DashboardMétriquesAlerte
Terraform StateDrift detection, plan changes, apply successDrift détecté → Slack
Ansible RunsPlaybook duration, task failures, changed hostsFailure > 0 → escalade
Configuration DriftÉcart entre état désiré et réelDrift > 5 % → investigation
Security CompliancePackages patched, firewall rules, audit rulesCVE 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

ItemSans IaCAvec IaCÉconomie
Provisionnement initial120 h (4h/serveur)2 h (terraform apply)118 h
Configuration initiale60 h (2h/serveur)1 h (ansible-playbook)59 h
Patching mensuel30 h (1h/serveur)15 min (1 playbook)29.75 h
Reprovisionnement DR120 h2 h118 h
Audit de configuration40 h (manuel)5 min (terraform plan + ansible —check)39.9 h
Total annuel2 340 h40 h2 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

JourActionLivrable
J1Installer Terraform + Ansible + GitEnvironnement IaC opérationnel
J2Modules Terraform (VM, réseau, stockage)Infrastructure provisionnée par code
J3Playbooks Ansible (hardening, services)Configuration automatisée et idempotente
J4Pipeline GitLab CI/CD + VaultGitOps avec approval gates
J5Dashboards Grafana + alertesMonitoring 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

  1. Quelle est la différence entre Terraform et Ansible ?
  2. L’IaC est-elle obligatoire pour la conformité NIS2 ?
  3. Combien de temps faut-il pour adopter l’IaC sur une infrastructure existante ?
  4. Comment gérer les secrets avec Terraform et Ansible ?
  5. Peut-on utiliser Terraform avec d’autres clouds qu’OpenNebula ?
  6. Qu’est-ce que le drift detection et pourquoi est-ce important ?
---

Restez informé de l'actualité cloud & IA

Recevez nos analyses, retours terrain et nouveautés produits. Pas de spam, pas de bruit.

En vous inscrivant, vous acceptez notre politique de confidentialité. Désinscription à tout moment.