Déployer et Gérer Docker avec Ansible

Déployez et gérez Docker avec Ansible : installation automatisée, modules docker_container, docker_network, docker_volume, stack complète Traefik + App + PostgreSQL + Redis.

Docker a révolutionné la manière dont nous déployons et gérons les applications. Mais lorsqu'on multiplie les serveurs et les environnements, le déploiement manuel de conteneurs devient rapidement ingérable. C'est là qu'Ansible entre en jeu : en combinant la puissance de Docker avec l'automatisation d'Ansible, vous obtenez une infrastructure conteneurisée reproductible, versionnable et déployable en un clic.

Dans cet article, nous allons explorer en profondeur comment utiliser Ansible pour installer Docker, gérer les images, conteneurs, réseaux et volumes, puis déployer une stack complète de production comprenant Traefik, une application web, PostgreSQL et Redis. Nous aborderons également la gestion des registries privés, la maintenance automatisée et les avantages par rapport à un docker-compose classique.

Architecture en un coup d'œil

Diagramme - Déployer et Gérer Docker avec Ansible

Prérequis et installation de la collection community.docker

Avant de pouvoir gérer Docker avec Ansible, il faut installer la collection community.docker. Cette collection fournit tous les modules nécessaires pour interagir avec le daemon Docker : gestion des conteneurs, images, réseaux, volumes et Docker Compose.

Installation de la collection

# Installer la collection community.docker
ansible-galaxy collection install community.docker

# Vérifier l'installation
ansible-galaxy collection list | grep docker

# Pour un projet, créez un fichier requirements.yml
cat > requirements.yml <<EOF
---
collections:
  - name: community.docker
    version: ">=3.8.0"
  - name: community.general
    version: ">=8.0.0"
EOF

# Installer depuis le fichier requirements
ansible-galaxy collection install -r requirements.yml

Prérequis Python sur les hôtes cibles

Les modules community.docker nécessitent le SDK Python Docker sur les machines cibles. Ce prérequis sera géré automatiquement dans notre playbook d'installation, mais il est important de le connaître :

  • docker (Python package) : version 7.0.0 ou supérieure
  • docker-compose (Python package) : pour le module docker_compose_v2, la CLI Docker Compose v2 doit être installée
  • Python 3.8+ sur les machines cibles

Structure du projet

docker-ansible/
├── inventory/
│   ├── hosts.yml
│   └── group_vars/
│       └── docker_hosts.yml
├── roles/
│   ├── docker-install/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── handlers/
│   │       └── main.yml
│   ├── docker-stack/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   ├── templates/
│   │   │   ├── docker-compose.yml.j2
│   │   │   ├── traefik.yml.j2
│   │   │   └── .env.j2
│   │   └── handlers/
│   │       └── main.yml
│   └── docker-maintenance/
│       └── tasks/
│           └── main.yml
├── requirements.yml
└── site.yml

Installer Docker via Ansible : playbook complet

L'installation de Docker via les paquets officiels nécessite plusieurs étapes : ajout du dépôt GPG, configuration des sources APT, installation du moteur et de ses plugins. Notre rôle Ansible automatise tout cela de manière idempotente.

Variables de configuration

# inventory/group_vars/docker_hosts.yml
---
# Configuration Docker
docker_edition: "ce"
docker_version: ""  # Vide = dernière version stable
docker_compose_version: "v2.24.5"
docker_users:
  - deployer

# Configuration réseau Docker
docker_daemon_config:
  log-driver: "json-file"
  log-opts:
    max-size: "50m"
    max-file: "3"
  default-address-pools:
    - base: "172.20.0.0/16"
      size: 24
  storage-driver: "overlay2"
  live-restore: true
  userland-proxy: false
  experimental: false
  metrics-addr: "127.0.0.1:9323"

# Configuration de la stack applicative
app_name: "monapp"
app_domain: "app.codeclan.fr"
app_image: "registry.codeclan.fr/monapp:latest"
app_port: 8000
app_replicas: 2

# Traefik
traefik_dashboard_domain: "traefik.codeclan.fr"
traefik_acme_email: "admin@codeclan.fr"
traefik_dashboard_auth: "admin:$apr1$xyz123$hashedpassword"

# PostgreSQL
postgres_version: "16"
postgres_db: "monapp_db"
postgres_user: "monapp_user"
postgres_password: "{{ vault_postgres_password }}"
postgres_max_connections: 100

# Redis
redis_version: "7-alpine"
redis_password: "{{ vault_redis_password }}"
redis_maxmemory: "256mb"

# Registry privé
docker_registry_url: "registry.codeclan.fr"
docker_registry_username: "{{ vault_registry_username }}"
docker_registry_password: "{{ vault_registry_password }}"

# Maintenance
docker_prune_schedule: "0 3 * * 0"  # Dimanche à 3h
docker_image_update_schedule: "0 4 * * 1"  # Lundi à 4h

Rôle docker-install

# roles/docker-install/tasks/main.yml
---
- name: Supprimer les anciennes versions de Docker
  ansible.builtin.apt:
    name:
      - docker
      - docker-engine
      - docker.io
      - containerd
      - runc
    state: absent

- name: Installer les prérequis système
  ansible.builtin.apt:
    name:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
      - lsb-release
      - python3-pip
      - python3-setuptools
      - python3-venv
    state: present
    update_cache: yes

- name: Créer le répertoire pour la clé GPG Docker
  ansible.builtin.file:
    path: /etc/apt/keyrings
    state: directory
    mode: "0755"

- name: Ajouter la clé GPG officielle Docker
  ansible.builtin.get_url:
    url: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
    dest: /etc/apt/keyrings/docker.asc
    mode: "0644"

- name: Ajouter le dépôt Docker APT
  ansible.builtin.apt_repository:
    repo: >-
      deb [arch={{ docker_arch }} signed-by=/etc/apt/keyrings/docker.asc]
      https://download.docker.com/linux/{{ ansible_distribution | lower }}
      {{ ansible_distribution_release }} stable
    state: present
    filename: docker
  vars:
    docker_arch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"

- name: Installer Docker Engine et ses composants
  ansible.builtin.apt:
    name:
      - "docker-{{ docker_edition }}"
      - "docker-{{ docker_edition }}-cli"
      - containerd.io
      - docker-buildx-plugin
      - docker-compose-plugin
    state: present
    update_cache: yes
  notify: Redémarrer Docker

- name: Installer le SDK Python Docker
  ansible.builtin.pip:
    name:
      - docker>=7.0.0
      - docker-compose
    state: present

- name: Ajouter les utilisateurs au groupe docker
  ansible.builtin.user:
    name: "{{ item }}"
    groups: docker
    append: yes
  loop: "{{ docker_users }}"

- name: Créer le répertoire de configuration Docker
  ansible.builtin.file:
    path: /etc/docker
    state: directory
    owner: root
    group: root
    mode: "0755"

- name: Configurer le daemon Docker
  ansible.builtin.copy:
    content: "{{ docker_daemon_config | to_nice_json }}"
    dest: /etc/docker/daemon.json
    owner: root
    group: root
    mode: "0644"
  notify: Redémarrer Docker

- name: Activer et démarrer Docker
  ansible.builtin.systemd:
    name: docker
    state: started
    enabled: yes

- name: Activer le service containerd
  ansible.builtin.systemd:
    name: containerd
    state: started
    enabled: yes

- name: Vérifier l'installation de Docker
  ansible.builtin.command:
    cmd: docker version
  register: docker_version_output
  changed_when: false

- name: Afficher la version de Docker
  ansible.builtin.debug:
    msg: "{{ docker_version_output.stdout_lines[:6] }}"

- name: Vérifier Docker Compose
  ansible.builtin.command:
    cmd: docker compose version
  register: compose_version_output
  changed_when: false

- name: Afficher la version de Docker Compose
  ansible.builtin.debug:
    msg: "{{ compose_version_output.stdout }}"

Handlers docker-install

# roles/docker-install/handlers/main.yml
---
- name: Redémarrer Docker
  ansible.builtin.systemd:
    name: docker
    state: restarted
    daemon_reload: yes

Les modules essentiels de community.docker

La collection community.docker fournit une dizaine de modules. Découvrons les plus importants avec des exemples concrets et détaillés.

docker_image : gérer les images

Le module docker_image permet de pull, build et supprimer des images Docker.

# Exemples d'utilisation de docker_image
- name: Télécharger une image depuis Docker Hub
  community.docker.docker_image:
    name: nginx
    tag: "1.25-alpine"
    source: pull

- name: Construire une image depuis un Dockerfile
  community.docker.docker_image:
    name: "{{ docker_registry_url }}/{{ app_name }}"
    tag: "{{ app_version | default('latest') }}"
    source: build
    build:
      path: /opt/app
      dockerfile: Dockerfile
      pull: yes
      args:
        BUILD_ENV: production
    push: yes

- name: Supprimer une image obsolète
  community.docker.docker_image:
    name: "old-app"
    tag: "v1.0"
    state: absent

- name: Télécharger plusieurs images en boucle
  community.docker.docker_image:
    name: "{{ item.name }}"
    tag: "{{ item.tag }}"
    source: pull
  loop:
    - { name: "postgres", tag: "16-alpine" }
    - { name: "redis", tag: "7-alpine" }
    - { name: "traefik", tag: "v3.0" }
  loop_control:
    label: "{{ item.name }}:{{ item.tag }}"

docker_network : gérer les réseaux

Les réseaux Docker permettent d'isoler les communications entre conteneurs. Le module docker_network gère leur cycle de vie complet.

# Exemples d'utilisation de docker_network
- name: Créer le réseau frontend (accessible depuis Traefik)
  community.docker.docker_network:
    name: frontend
    driver: bridge
    ipam_config:
      - subnet: 172.20.1.0/24
        gateway: 172.20.1.1
    state: present

- name: Créer le réseau backend (interne uniquement)
  community.docker.docker_network:
    name: backend
    driver: bridge
    internal: yes
    ipam_config:
      - subnet: 172.20.2.0/24
    state: present

- name: Créer un réseau avec des labels
  community.docker.docker_network:
    name: monitoring
    driver: bridge
    labels:
      environment: production
      managed_by: ansible
    state: present

docker_volume : gérer les volumes

Les volumes Docker permettent de persister les données en dehors du cycle de vie des conteneurs. Ce module gère leur création et leur configuration.

# Exemples d'utilisation de docker_volume
- name: Créer le volume pour PostgreSQL
  community.docker.docker_volume:
    name: postgres_data
    driver: local
    driver_options:
      type: none
      o: bind
      device: /opt/docker/data/postgres
    labels:
      service: postgresql
      backup: "true"
    state: present

- name: Créer le volume pour Redis
  community.docker.docker_volume:
    name: redis_data
    driver: local
    state: present

- name: Créer le volume pour les certificats TLS
  community.docker.docker_volume:
    name: traefik_certs
    driver: local
    state: present

- name: Créer les répertoires hôtes pour les bind mounts
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: "1000"
    group: "1000"
    mode: "0755"
  loop:
    - /opt/docker/data/postgres
    - /opt/docker/data/redis
    - /opt/docker/config/traefik
    - /opt/docker/logs

docker_container : gérer les conteneurs

Le module docker_container est le plus utilisé. Il permet de créer, démarrer, arrêter et supprimer des conteneurs avec un contrôle fin de tous les paramètres.

# Exemples d'utilisation de docker_container
- name: Déployer un conteneur PostgreSQL
  community.docker.docker_container:
    name: postgres
    image: "postgres:{{ postgres_version }}-alpine"
    state: started
    restart_policy: unless-stopped
    env:
      POSTGRES_DB: "{{ postgres_db }}"
      POSTGRES_USER: "{{ postgres_user }}"
      POSTGRES_PASSWORD: "{{ postgres_password }}"
      POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - /opt/docker/config/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - name: backend
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    memory: "512m"
    memory_swap: "1g"
    cpu_period: 100000
    cpu_quota: 50000
    labels:
      service: postgresql
      managed_by: ansible

- name: Déployer un conteneur Redis
  community.docker.docker_container:
    name: redis
    image: "redis:{{ redis_version }}"
    state: started
    restart_policy: unless-stopped
    command: >
      redis-server
      --requirepass {{ redis_password }}
      --maxmemory {{ redis_maxmemory }}
      --maxmemory-policy allkeys-lru
      --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - name: backend
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "{{ redis_password }}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    memory: "512m"
    labels:
      service: redis
      managed_by: ansible

- name: Arrêter un conteneur proprement
  community.docker.docker_container:
    name: old-service
    state: stopped
    stop_timeout: 30

- name: Supprimer un conteneur
  community.docker.docker_container:
    name: old-service
    state: absent

docker_compose_v2 : orchestration multi-conteneurs

Le module docker_compose_v2 permet d'utiliser Docker Compose directement depuis Ansible. C'est la méthode recommandée pour déployer des stacks complexes avec plusieurs services interdépendants.

# Utilisation de docker_compose_v2
- name: Déployer la stack via Docker Compose
  community.docker.docker_compose_v2:
    project_src: /opt/docker/{{ app_name }}
    state: present
    pull: always
    build: never
    remove_orphans: yes
  register: compose_result

- name: Afficher les services déployés
  ansible.builtin.debug:
    msg: "{{ compose_result }}"

- name: Arrêter la stack
  community.docker.docker_compose_v2:
    project_src: /opt/docker/{{ app_name }}
    state: absent
    remove_volumes: no

Gérer le cycle de vie des conteneurs

La gestion du cycle de vie des conteneurs est un aspect crucial du déploiement Docker en production. Ansible permet d'orchestrer les mises à jour avec des stratégies de déploiement sophistiquées.

Rolling update

# Mise à jour progressive des conteneurs
- name: Télécharger la nouvelle version de l'image
  community.docker.docker_image:
    name: "{{ app_image }}"
    source: pull
    force_source: yes
  register: image_pull

- name: Recréer le conteneur si l'image a changé
  community.docker.docker_container:
    name: "{{ app_name }}"
    image: "{{ app_image }}"
    state: started
    restart_policy: unless-stopped
    recreate: "{{ image_pull.changed }}"
    env:
      DATABASE_URL: "postgresql://{{ postgres_user }}:{{ postgres_password }}@postgres:5432/{{ postgres_db }}"
      REDIS_URL: "redis://:{{ redis_password }}@redis:6379/0"
    networks:
      - name: frontend
      - name: backend
    labels:
      traefik.enable: "true"
      traefik.http.routers.app.rule: "Host(`{{ app_domain }}`)"
      traefik.http.routers.app.tls.certresolver: "letsencrypt"
      traefik.http.services.app.loadbalancer.server.port: "{{ app_port }}"

Health checks et monitoring

# Vérification de la santé des conteneurs
- name: Vérifier l'état de santé de tous les conteneurs
  community.docker.docker_container_info:
    name: "{{ item }}"
  register: container_info
  loop:
    - postgres
    - redis
    - "{{ app_name }}"
    - traefik

- name: Alerter si un conteneur n'est pas healthy
  ansible.builtin.debug:
    msg: >
      ATTENTION : Le conteneur {{ item.item }} est en état
      {{ item.container.State.Health.Status | default('unknown') }}
  when: >
    item.container.State.Health is defined and
    item.container.State.Health.Status != 'healthy'
  loop: "{{ container_info.results }}"
  loop_control:
    label: "{{ item.item }}"

- name: Récupérer les logs d'un conteneur en erreur
  community.docker.docker_container_info:
    name: "{{ app_name }}"
  register: app_info

- name: Afficher les logs récents si le conteneur redémarre
  ansible.builtin.command:
    cmd: "docker logs --tail 50 {{ app_name }}"
  when: app_info.container.RestartCount | int > 0
  register: app_logs
  changed_when: false

Déployer une stack complète : Traefik + App + PostgreSQL + Redis

Passons maintenant au déploiement concret d'une stack de production complète. Cette stack comprend Traefik comme reverse proxy avec terminaison TLS automatique, une application web, PostgreSQL comme base de données et Redis comme cache.

Architecture de la stack

Notre architecture utilise deux réseaux Docker isolés :

  • frontend : connecte Traefik à l'application (accessible depuis l'extérieur)
  • backend : connecte l'application à PostgreSQL et Redis (réseau interne, isolé)

Cette séparation garantit que les bases de données ne sont jamais exposées directement à internet.

Rôle docker-stack

# roles/docker-stack/tasks/main.yml
---
- name: Créer les répertoires de la stack
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: docker
    mode: "0750"
  loop:
    - "/opt/docker/{{ app_name }}"
    - "/opt/docker/{{ app_name }}/config"
    - "/opt/docker/{{ app_name }}/config/traefik"
    - "/opt/docker/{{ app_name }}/config/traefik/dynamic"
    - "/opt/docker/{{ app_name }}/data"
    - "/opt/docker/{{ app_name }}/data/postgres"
    - "/opt/docker/{{ app_name }}/data/redis"
    - "/opt/docker/{{ app_name }}/data/traefik"
    - "/opt/docker/{{ app_name }}/logs"
    - "/opt/docker/{{ app_name }}/backups"

# -------------------------
# Réseaux Docker
# -------------------------
- name: Créer le réseau frontend
  community.docker.docker_network:
    name: "{{ app_name }}_frontend"
    driver: bridge
    state: present

- name: Créer le réseau backend (interne)
  community.docker.docker_network:
    name: "{{ app_name }}_backend"
    driver: bridge
    internal: yes
    state: present

# -------------------------
# Volumes Docker
# -------------------------
- name: Créer les volumes persistants
  community.docker.docker_volume:
    name: "{{ item }}"
    state: present
  loop:
    - "{{ app_name }}_postgres_data"
    - "{{ app_name }}_redis_data"
    - "{{ app_name }}_traefik_certs"

# -------------------------
# Configuration Traefik
# -------------------------
- name: Déployer la configuration statique de Traefik
  ansible.builtin.template:
    src: traefik.yml.j2
    dest: "/opt/docker/{{ app_name }}/config/traefik/traefik.yml"
    owner: root
    group: docker
    mode: "0640"
  notify: Redémarrer Traefik

- name: Déployer la configuration dynamique de Traefik
  ansible.builtin.copy:
    content: |
      # Ansible managed
      http:
        middlewares:
          security-headers:
            headers:
              browserXssFilter: true
              contentTypeNosniff: true
              frameDeny: true
              stsIncludeSubdomains: true
              stsPreload: true
              stsSeconds: 31536000
              customFrameOptionsValue: "SAMEORIGIN"
              customResponseHeaders:
                X-Powered-By: ""
                Server: ""
          rate-limit:
            rateLimit:
              average: 100
              burst: 50
              period: 1s
          compress:
            compress:
              excludedContentTypes:
                - text/event-stream
    dest: "/opt/docker/{{ app_name }}/config/traefik/dynamic/middlewares.yml"
    owner: root
    group: docker
    mode: "0640"
  notify: Redémarrer Traefik

# -------------------------
# Déploiement des conteneurs
# -------------------------
- name: Déployer Traefik (reverse proxy)
  community.docker.docker_container:
    name: "{{ app_name }}_traefik"
    image: "traefik:v3.0"
    state: started
    restart_policy: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "/opt/docker/{{ app_name }}/config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
      - "/opt/docker/{{ app_name }}/config/traefik/dynamic:/etc/traefik/dynamic:ro"
      - "{{ app_name }}_traefik_certs:/letsencrypt"
      - "/opt/docker/{{ app_name }}/logs/traefik:/var/log/traefik"
    networks:
      - name: "{{ app_name }}_frontend"
    labels:
      traefik.enable: "true"
      traefik.http.routers.dashboard.rule: "Host(`{{ traefik_dashboard_domain }}`)"
      traefik.http.routers.dashboard.service: "api@internal"
      traefik.http.routers.dashboard.tls.certresolver: "letsencrypt"
      traefik.http.routers.dashboard.middlewares: "dashboard-auth"
      traefik.http.middlewares.dashboard-auth.basicauth.users: "{{ traefik_dashboard_auth }}"
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    memory: "256m"

- name: Déployer PostgreSQL
  community.docker.docker_container:
    name: "{{ app_name }}_postgres"
    image: "postgres:{{ postgres_version }}-alpine"
    state: started
    restart_policy: unless-stopped
    env:
      POSTGRES_DB: "{{ postgres_db }}"
      POSTGRES_USER: "{{ postgres_user }}"
      POSTGRES_PASSWORD: "{{ postgres_password }}"
      POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --encoding=UTF8 --locale=fr_FR.UTF-8"
    volumes:
      - "{{ app_name }}_postgres_data:/var/lib/postgresql/data"
    networks:
      - name: "{{ app_name }}_backend"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    memory: "1g"
    memory_swap: "2g"
    shm_size: "256m"
    labels:
      managed_by: ansible
      service: postgresql
      backup: "true"

- name: Déployer Redis
  community.docker.docker_container:
    name: "{{ app_name }}_redis"
    image: "redis:{{ redis_version }}"
    state: started
    restart_policy: unless-stopped
    command: >
      redis-server
      --requirepass {{ redis_password }}
      --maxmemory {{ redis_maxmemory }}
      --maxmemory-policy allkeys-lru
      --appendonly yes
      --appendfsync everysec
      --save 900 1
      --save 300 10
      --save 60 10000
    volumes:
      - "{{ app_name }}_redis_data:/data"
    networks:
      - name: "{{ app_name }}_backend"
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "{{ redis_password }}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    memory: "512m"
    labels:
      managed_by: ansible
      service: redis

- name: Attendre que PostgreSQL soit prêt
  community.docker.docker_container_info:
    name: "{{ app_name }}_postgres"
  register: pg_info
  until: pg_info.container.State.Health.Status == "healthy"
  retries: 30
  delay: 5

- name: Attendre que Redis soit prêt
  community.docker.docker_container_info:
    name: "{{ app_name }}_redis"
  register: redis_info
  until: redis_info.container.State.Health.Status == "healthy"
  retries: 15
  delay: 3

- name: Se connecter au registry privé
  community.docker.docker_login:
    registry_url: "{{ docker_registry_url }}"
    username: "{{ docker_registry_username }}"
    password: "{{ docker_registry_password }}"
    reauthorize: yes
  when: docker_registry_url is defined

- name: Télécharger l'image de l'application
  community.docker.docker_image:
    name: "{{ app_image }}"
    source: pull
    force_source: yes
  register: app_image_pull

- name: Déployer l'application
  community.docker.docker_container:
    name: "{{ app_name }}_app"
    image: "{{ app_image }}"
    state: started
    restart_policy: unless-stopped
    env:
      DATABASE_URL: "postgresql://{{ postgres_user }}:{{ postgres_password }}@{{ app_name }}_postgres:5432/{{ postgres_db }}"
      REDIS_URL: "redis://:{{ redis_password }}@{{ app_name }}_redis:6379/0"
      APP_ENV: "production"
      APP_DEBUG: "false"
      APP_PORT: "{{ app_port | string }}"
    volumes:
      - "/opt/docker/{{ app_name }}/logs/app:/app/logs"
    networks:
      - name: "{{ app_name }}_frontend"
      - name: "{{ app_name }}_backend"
    labels:
      traefik.enable: "true"
      traefik.http.routers.app.rule: "Host(`{{ app_domain }}`)"
      traefik.http.routers.app.tls.certresolver: "letsencrypt"
      traefik.http.routers.app.middlewares: "security-headers@file,compress@file,rate-limit@file"
      traefik.http.services.app.loadbalancer.server.port: "{{ app_port | string }}"
      traefik.docker.network: "{{ app_name }}_frontend"
      managed_by: ansible
      service: app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}/health"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 45s
    memory: "512m"
    memory_swap: "1g"

- name: Vérifier que l'application est healthy
  community.docker.docker_container_info:
    name: "{{ app_name }}_app"
  register: app_info
  until: app_info.container.State.Health.Status == "healthy"
  retries: 30
  delay: 10

Template Traefik

# roles/docker-stack/templates/traefik.yml.j2
# Ansible managed - ne pas modifier manuellement
# Configuration statique de Traefik

api:
  dashboard: true
  insecure: false

ping:
  entryPoint: traefik

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
  traefik:
    address: ":8080"

certificatesResolvers:
  letsencrypt:
    acme:
      email: "{{ traefik_acme_email }}"
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: "{{ app_name }}_frontend"
    watch: true
  file:
    directory: /etc/traefik/dynamic
    watch: true

log:
  level: INFO
  filePath: /var/log/traefik/traefik.log

accessLog:
  filePath: /var/log/traefik/access.log
  bufferingSize: 100
  filters:
    statusCodes:
      - "400-499"
      - "500-599"

Handlers docker-stack

# roles/docker-stack/handlers/main.yml
---
- name: Redémarrer Traefik
  community.docker.docker_container:
    name: "{{ app_name }}_traefik"
    state: started
    restart: yes

Gestion des registries privés

En entreprise, vous utiliserez souvent un registry Docker privé pour stocker vos images. Ansible gère parfaitement l'authentification et les interactions avec ces registries.

# Gestion complète des registries privés
- name: Se connecter au registry privé
  community.docker.docker_login:
    registry_url: "{{ docker_registry_url }}"
    username: "{{ docker_registry_username }}"
    password: "{{ docker_registry_password }}"
    reauthorize: yes

- name: Construire et pousser une image vers le registry
  community.docker.docker_image:
    name: "{{ docker_registry_url }}/{{ app_name }}"
    tag: "{{ lookup('pipe', 'git rev-parse --short HEAD') }}"
    source: build
    build:
      path: /opt/app
      pull: yes
      nocache: no
    push: yes

- name: Taguer l'image comme latest
  community.docker.docker_image:
    name: "{{ docker_registry_url }}/{{ app_name }}"
    tag: "{{ lookup('pipe', 'git rev-parse --short HEAD') }}"
    repository: "{{ docker_registry_url }}/{{ app_name }}"
    push: yes
    source: local

- name: Se déconnecter du registry
  community.docker.docker_login:
    registry_url: "{{ docker_registry_url }}"
    state: absent
Conseil : Utilisez toujours Ansible Vault pour stocker les identifiants de votre registry privé. Ne stockez jamais de mots de passe en clair dans vos fichiers de variables.

Maintenance Docker automatisée

La maintenance régulière est essentielle pour éviter l'accumulation d'images, conteneurs et volumes inutilisés qui gaspillent de l'espace disque.

Rôle docker-maintenance

# roles/docker-maintenance/tasks/main.yml
---
# -------------------------
# Nettoyage Docker (prune)
# -------------------------
- name: Déployer le script de maintenance Docker
  ansible.builtin.copy:
    content: |
      #!/bin/bash
      # Ansible managed - Script de maintenance Docker
      set -euo pipefail

      echo "=== Maintenance Docker - $(date) ==="

      # Supprimer les conteneurs arrêtés
      echo "Nettoyage des conteneurs arrêtés..."
      docker container prune -f

      # Supprimer les images non utilisées (dangling)
      echo "Nettoyage des images dangling..."
      docker image prune -f

      # Supprimer les images non référencées de plus de 24h
      echo "Nettoyage des images non utilisées..."
      docker image prune -a -f --filter "until=24h"

      # Supprimer les volumes orphelins
      echo "Nettoyage des volumes orphelins..."
      docker volume prune -f

      # Supprimer les réseaux non utilisés
      echo "Nettoyage des réseaux non utilisés..."
      docker network prune -f

      # Supprimer le cache de build
      echo "Nettoyage du cache de build..."
      docker builder prune -f --keep-storage=5GB

      # Afficher l'espace récupéré
      echo "=== Utilisation disque Docker ==="
      docker system df

      echo "=== Maintenance terminée - $(date) ==="
    dest: /usr/local/bin/docker-maintenance.sh
    owner: root
    group: root
    mode: "0755"

- name: Planifier la maintenance Docker via cron
  ansible.builtin.cron:
    name: "Maintenance Docker hebdomadaire"
    job: "/usr/local/bin/docker-maintenance.sh >> /var/log/docker-maintenance.log 2>&1"
    minute: "{{ docker_prune_schedule.split(' ')[0] }}"
    hour: "{{ docker_prune_schedule.split(' ')[1] }}"
    day: "{{ docker_prune_schedule.split(' ')[2] }}"
    month: "{{ docker_prune_schedule.split(' ')[3] }}"
    weekday: "{{ docker_prune_schedule.split(' ')[4] }}"
    user: root

# -------------------------
# Mise à jour des images
# -------------------------
- name: Déployer le script de mise à jour des images
  ansible.builtin.copy:
    content: |
      #!/bin/bash
      # Ansible managed - Script de mise à jour des images Docker
      set -euo pipefail

      echo "=== Mise à jour des images Docker - $(date) ==="

      # Lister toutes les images en cours d'utilisation
      IMAGES=$(docker ps --format '{{ '{{' }}.Image{{ '}}' }}' | sort -u)

      for IMAGE in $IMAGES; do
        echo "Vérification de $IMAGE..."
        docker pull "$IMAGE" 2>/dev/null && echo "  -> $IMAGE mis à jour" || echo "  -> $IMAGE inchangé"
      done

      echo "=== Mise à jour terminée - $(date) ==="
      echo "NOTE: Redémarrez les conteneurs pour appliquer les nouvelles images"
    dest: /usr/local/bin/docker-update-images.sh
    owner: root
    group: root
    mode: "0755"

- name: Planifier la vérification des mises à jour
  ansible.builtin.cron:
    name: "Vérification des mises à jour Docker"
    job: "/usr/local/bin/docker-update-images.sh >> /var/log/docker-updates.log 2>&1"
    minute: "{{ docker_image_update_schedule.split(' ')[0] }}"
    hour: "{{ docker_image_update_schedule.split(' ')[1] }}"
    day: "{{ docker_image_update_schedule.split(' ')[2] }}"
    month: "{{ docker_image_update_schedule.split(' ')[3] }}"
    weekday: "{{ docker_image_update_schedule.split(' ')[4] }}"
    user: root

# -------------------------
# Sauvegarde des données Docker
# -------------------------
- name: Déployer le script de sauvegarde PostgreSQL
  ansible.builtin.copy:
    content: |
      #!/bin/bash
      # Ansible managed - Sauvegarde PostgreSQL
      set -euo pipefail

      BACKUP_DIR="/opt/docker/{{ app_name }}/backups"
      DATE=$(date +%Y%m%d_%H%M%S)
      RETENTION_DAYS=14

      echo "Sauvegarde PostgreSQL - $(date)"

      docker exec {{ app_name }}_postgres pg_dumpall \
        -U {{ postgres_user }} \
        --clean \
        | gzip > "${BACKUP_DIR}/postgres_${DATE}.sql.gz"

      echo "Sauvegarde créée : postgres_${DATE}.sql.gz"
      echo "Taille : $(du -h ${BACKUP_DIR}/postgres_${DATE}.sql.gz | cut -f1)"

      # Rotation des sauvegardes
      find "${BACKUP_DIR}" -name "postgres_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
      echo "Sauvegardes de plus de ${RETENTION_DAYS} jours supprimées"
    dest: /usr/local/bin/docker-backup-postgres.sh
    owner: root
    group: root
    mode: "0755"

- name: Planifier la sauvegarde PostgreSQL quotidienne
  ansible.builtin.cron:
    name: "Sauvegarde PostgreSQL quotidienne"
    job: "/usr/local/bin/docker-backup-postgres.sh >> /var/log/docker-backup.log 2>&1"
    minute: "0"
    hour: "2"
    user: root

# -------------------------
# Monitoring Docker
# -------------------------
- name: Déployer le script de vérification de santé
  ansible.builtin.copy:
    content: |
      #!/bin/bash
      # Ansible managed - Health check des conteneurs Docker
      set -euo pipefail

      UNHEALTHY=$(docker ps --filter "health=unhealthy" --format "{{ '{{' }}.Names{{ '}}' }}: {{ '{{' }}.Status{{ '}}' }}")
      EXITED=$(docker ps -a --filter "status=exited" --filter "label=managed_by=ansible" --format "{{ '{{' }}.Names{{ '}}' }}: exited {{ '{{' }}.Status{{ '}}' }}")

      if [ -n "$UNHEALTHY" ]; then
        echo "ALERTE - Conteneurs unhealthy:"
        echo "$UNHEALTHY"
        # Envoyer une notification (webhook, email, etc.)
      fi

      if [ -n "$EXITED" ]; then
        echo "ALERTE - Conteneurs arrêtés:"
        echo "$EXITED"
      fi

      if [ -z "$UNHEALTHY" ] && [ -z "$EXITED" ]; then
        echo "OK - Tous les conteneurs sont en bonne santé"
      fi
    dest: /usr/local/bin/docker-healthcheck.sh
    owner: root
    group: root
    mode: "0755"

- name: Planifier la vérification de santé toutes les 5 minutes
  ansible.builtin.cron:
    name: "Health check Docker"
    job: "/usr/local/bin/docker-healthcheck.sh >> /var/log/docker-healthcheck.log 2>&1"
    minute: "*/5"
    user: root

- name: Configurer logrotate pour les logs Docker de maintenance
  ansible.builtin.copy:
    content: |
      # Ansible managed
      /var/log/docker-maintenance.log
      /var/log/docker-updates.log
      /var/log/docker-backup.log
      /var/log/docker-healthcheck.log {
          weekly
          rotate 4
          compress
          missingok
          notifempty
      }
    dest: /etc/logrotate.d/docker-maintenance
    owner: root
    group: root
    mode: "0644"

Le playbook complet de déploiement

Voici le playbook principal qui orchestre l'installation de Docker, le déploiement de la stack et la configuration de la maintenance :

# site.yml
---
- name: Installer Docker et déployer la stack applicative
  hosts: docker_hosts
  become: yes
  gather_facts: yes

  pre_tasks:
    - name: Vérifier la connectivité
      ansible.builtin.ping:

    - name: Vérifier les prérequis système
      ansible.builtin.assert:
        that:
          - ansible_distribution in ['Debian', 'Ubuntu']
          - ansible_memtotal_mb >= 2048
          - ansible_processor_vcpus >= 2
        fail_msg: >
          Prérequis non remplis.
          OS supporté: Debian/Ubuntu.
          RAM minimum: 2 Go (actuel: {{ ansible_memtotal_mb }} Mo).
          CPU minimum: 2 vCPU (actuel: {{ ansible_processor_vcpus }}).

    - name: Afficher les informations du serveur
      ansible.builtin.debug:
        msg: >
          {{ inventory_hostname }} |
          {{ ansible_distribution }} {{ ansible_distribution_version }} |
          {{ ansible_processor_vcpus }} vCPU |
          {{ ansible_memtotal_mb }} Mo RAM |
          Disque: {{ ansible_mounts[0].size_available | human_readable }}

  roles:
    - role: docker-install
      tags: [docker, install]

    - role: docker-stack
      tags: [docker, stack, deploy]

    - role: docker-maintenance
      tags: [docker, maintenance]

  post_tasks:
    - name: Afficher l'état final de la stack
      ansible.builtin.command:
        cmd: docker ps --format "table {{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Image{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}\t{{ '{{' }}.Ports{{ '}}' }}"
      register: docker_ps
      changed_when: false

    - name: Résumé du déploiement
      ansible.builtin.debug:
        msg:
          - "============================================"
          - "Stack {{ app_name }} déployée avec succès"
          - "============================================"
          - ""
          - "{{ docker_ps.stdout }}"
          - ""
          - "============================================"
          - "URLs :"
          - "  Application : https://{{ app_domain }}"
          - "  Traefik     : https://{{ traefik_dashboard_domain }}"
          - "============================================"
          - "Maintenance :"
          - "  Prune    : {{ docker_prune_schedule }}"
          - "  Updates  : {{ docker_image_update_schedule }}"
          - "  Backup   : Quotidien à 2h"
          - "============================================"

Comparaison : Ansible vs docker-compose direct

Une question légitime se pose : pourquoi ne pas simplement utiliser docker-compose up -d directement ? Voici une comparaison détaillée pour vous aider à choisir la bonne approche selon votre contexte.

Avantages d'Ansible par rapport à docker-compose direct

  1. Installation de Docker incluse : Ansible peut provisionner un serveur vierge, installer Docker et déployer la stack en une seule exécution. Docker Compose nécessite que Docker soit déjà installé.
  2. Gestion multi-serveurs : Ansible déploie sur N serveurs en parallèle. Avec docker-compose, il faut se connecter à chaque serveur individuellement.
  3. Gestion des secrets : Ansible Vault chiffre les variables sensibles directement dans le code source. Docker Compose nécessite des fichiers .env non chiffrés ou des outils tiers.
  4. Pré/post-tâches : vérification des prérequis, migrations de base de données, tests de santé, notifications... Ansible gère toute la chaîne de déploiement.
  5. Idempotence robuste : Ansible vérifie l'état actuel avant d'appliquer des changements. Un docker-compose up -d recrée parfois des conteneurs inutilement.
  6. Configuration système : pare-feu, utilisateurs, sysctl, kernel parameters... Ansible configure tout l'environnement, pas seulement les conteneurs.
  7. Audit et traçabilité : chaque exécution d'Ansible produit un log détaillé des changements appliqués.

Quand docker-compose suffit

  • Développement local : pour lancer une stack sur votre machine, docker-compose est plus rapide et plus simple.
  • Serveur unique : si vous ne gérez qu'un seul serveur avec une stack simple, l'overhead d'Ansible n'est pas justifié.
  • Prototypage rapide : pour tester une idée rapidement, docker-compose est imbattable en termes de vitesse de mise en place.

L'approche hybride recommandée

La meilleure stratégie est souvent une approche hybride : utilisez docker-compose pour définir la stack (le fichier docker-compose.yml reste la source de vérité) et Ansible pour l'orchestration (installation, déploiement, configuration système, maintenance). C'est exactement ce que le module docker_compose_v2 permet de faire.

# Approche hybride : docker-compose.yml géré par Ansible
- name: Déployer le docker-compose.yml depuis un template
  ansible.builtin.template:
    src: docker-compose.yml.j2
    dest: "/opt/docker/{{ app_name }}/docker-compose.yml"
    owner: root
    group: docker
    mode: "0640"

- name: Déployer la stack via Docker Compose
  community.docker.docker_compose_v2:
    project_src: "/opt/docker/{{ app_name }}"
    state: present
    pull: always
    remove_orphans: yes
  register: stack_result

- name: Afficher les changements
  ansible.builtin.debug:
    var: stack_result.actions
  when: stack_result.changed

Commandes d'exécution

Voici les commandes essentielles pour utiliser ce playbook :

# Installer les collections requises
ansible-galaxy collection install -r requirements.yml

# Déploiement complet (install Docker + stack + maintenance)
ansible-playbook -i inventory/hosts.yml site.yml

# Installer Docker uniquement
ansible-playbook -i inventory/hosts.yml site.yml --tags install

# Déployer/mettre à jour la stack uniquement
ansible-playbook -i inventory/hosts.yml site.yml --tags deploy

# Mode dry-run
ansible-playbook -i inventory/hosts.yml site.yml --check --diff

# Avec Ansible Vault pour les secrets
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass

# Sur un serveur spécifique
ansible-playbook -i inventory/hosts.yml site.yml --limit web01

Bonnes pratiques

Pour conclure, voici les bonnes pratiques essentielles à suivre lorsque vous gérez Docker avec Ansible :

  • Utilisez toujours des tags d'images spécifiques en production (jamais :latest). Cela garantit la reproductibilité des déploiements.
  • Chiffrez vos secrets avec Ansible Vault. Les mots de passe, tokens et clés API ne doivent jamais être en clair dans vos fichiers.
  • Définissez des limites de ressources (mémoire, CPU) pour chaque conteneur afin d'éviter qu'un service défaillant ne consomme toutes les ressources du serveur.
  • Implémentez des health checks sur tous vos conteneurs. Sans health check, Docker ne peut pas détecter un service qui ne répond plus.
  • Séparez les réseaux : utilisez des réseaux internes pour les communications entre services et n'exposez que les ports strictement nécessaires.
  • Sauvegardez vos volumes régulièrement. Les données dans les conteneurs sont éphémères par défaut.
  • Versionnez tout : playbooks, variables, templates. Votre infrastructure doit pouvoir être reconstruite à l'identique à partir de Git.
  • Testez en staging avant de déployer en production. Molecule est un excellent outil pour tester vos rôles Ansible.

Conclusion

La combinaison Ansible et Docker offre une approche puissante et flexible pour gérer une infrastructure conteneurisée. Ansible apporte la reproductibilité, la gestion multi-serveurs et l'orchestration complète du cycle de déploiement, tandis que Docker fournit l'isolation et la portabilité des applications.

Dans cet article, nous avons couvert l'installation automatisée de Docker, l'utilisation détaillée des modules community.docker, le déploiement d'une stack de production complète avec Traefik, PostgreSQL et Redis, la gestion des registries privés et la maintenance automatisée. Ce playbook constitue une base solide que vous pouvez adapter et étendre selon les besoins spécifiques de votre infrastructure.

L'investissement initial dans la création de ces playbooks est largement compensé par le temps gagné lors des déploiements suivants, la réduction des erreurs humaines et la capacité à reconstruire votre infrastructure en quelques minutes en cas de sinistre. C'est l'essence même de l'Infrastructure as Code.

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.