Aller au contenu
Infrastructure as Code medium

Build d'images en parallèle avec Ansible : 1 image par client/version

14 min de lecture

Logo Ansible

Cas réel : compiler la même application pour N clients × M versions, avec un paramétrage différent à chaque combinaison, sans dupliquer la pipeline et sans empiler des shell scripts. Ansible permet de modéliser ça proprement : 1 conteneur de build par combinaison client/version, exécutés en parallèle, avec les artefacts récupérés dans un volume partagé. Le contrôle de la concurrence se fait par serial: au play et forks: côté ansible.cfg.

À la fin de cette page vous saurez écrire ce playbook avec les modules 2026 (community.docker.docker_image_build et community.docker.docker_container, qui ont remplacé l’ancien docker_image: source: build plus monolithique), comment dimensionner le parallélisme sans saturer la machine de build, et l’alternative containers.podman pour un build rootless quand Docker n’est pas une option (RHEL durci, CI rootless, conformité ANSSI).

  • Modéliser un build N×M (N clients × M versions) via un inventaire Ansible groupé.
  • Utiliser community.docker.docker_image_build (module 2026) plutôt que l’ancien docker_image: source: build.
  • Attendre la fin d’un conteneur de build avec detach: false.
  • Récupérer les artefacts via un volume partagé (/ext).
  • Dimensionner le parallélisme avec serial: au play et forks: dans ansible.cfg.
  • Alterner vers containers.podman pour un build rootless (RHEL durci, CI sécurisée).
  • Avoir installé la collection : ansible-galaxy collection install community.docker.
  • Docker Engine (24+ ou Docker Desktop) ou Podman 4+ installé sur le control node.
  • La lib Python requests dans le venv Ansible (sinon docker_container plante avec Failed to import the required Python library).
  • Connaître les inventaires statiques YAML — la structure repose sur des groupes par client.

Vous avez une seule application mais elle se déploie chez plusieurs clients, chaque client ayant sa version validée et ses paramètres de compilation (locale, branding, intégrations). Sans Ansible, vous faites :

  • N pipelines GitLab dupliqués (un par client),
  • ou un script shell qui boucle et serialise tout (lent),
  • ou un fichier docker-compose.yml par client (verbeux, pas dynamique).

Avec Ansible : un seul playbook, un inventaire qui décrit les combinaisons, et le parallélisme natif d’Ansible (forks: + serial:) gère le reste. Ajouter un client = ajouter 3 lignes dans l’inventaire.

L’image de build accepte les paramètres en ARG et expose un répertoire /artefact pour déposer le résultat compilé.

FROM alpine:3.20
ARG VERSION=unknown
ARG CLIENT=unknown
ENV VERSION=${VERSION} CLIENT=${CLIENT}
RUN apk add --no-cache git python3 tar \
&& mkdir -p /artefact
# Compilation : exemple bidon — adapter à la vraie commande de build
CMD ["sh", "-c", "echo 'building ${CLIENT} ${VERSION}' > /artefact/info.txt \
&& tar -czf /ext/${CLIENT}-${VERSION}.tar.gz -C /artefact ."]

Notes importantes :

  • Pas de CMD invariant dans le Dockerfile si vous voulez pouvoir le surcharger côté Ansible — laissez CMD pointer vers le script de compilation par défaut, et surchargez avec command: côté docker_container si besoin.
  • /ext est le point de montage où Ansible montera le volume hôte pour récupérer l’archive.
  • Les secrets ne vont PAS en ARG. Un ARG GIT_TOKEN reste lisible dans docker history image:tag --no-trunc. Pour un token Git, voir la section Secrets.
inventory.ini
[builds:children]
client_a
client_b
[client_a]
build-a ansible_connection=local
[client_b]
build-b ansible_connection=local
[client_a:vars]
client_name=alpha
target_version=1.2.0
target_locale=fr_FR
[client_b:vars]
client_name=beta
target_version=2.0.0
target_locale=en_US

1 host = 1 combinaison à builder. Pas besoin que build-a et build-b soient des vraies machines : ansible_connection=local rend tout local au control node, et delegate_to: 127.0.0.1 côté tâche garantit que le build s’exécute sur le control node, pas sur la cible théorique.

---
- name: Build images en parallèle
hosts: builds
gather_facts: false
serial: 4
tasks:
- name: Build de l'image avec ARG client/version
community.docker.docker_image_build:
name: "build-{{ client_name }}"
tag: "{{ target_version }}"
path: "{{ playbook_dir }}"
args:
VERSION: "{{ target_version }}"
CLIENT: "{{ client_name }}"
nocache: true
delegate_to: 127.0.0.1
- name: Lancer le conteneur de compilation
community.docker.docker_container:
name: "build-{{ client_name }}-{{ target_version }}"
image: "build-{{ client_name }}:{{ target_version }}"
recreate: true
state: started
detach: false
volumes:
- "{{ playbook_dir }}/ext:/ext"
env:
VERSION: "{{ target_version }}"
CLIENT: "{{ client_name }}"
delegate_to: 127.0.0.1
register: build_result
- name: Vérifier que l'archive a bien été produite
ansible.builtin.stat:
path: "{{ playbook_dir }}/ext/{{ client_name }}-{{ target_version }}.tar.gz"
register: archive
delegate_to: 127.0.0.1
failed_when: not archive.stat.exists

Lancement :

Fenêtre de terminal
mkdir -p ext
ansible-playbook -i inventory.ini playbook.yml

Avec serial: 4 et 2 hosts, les 2 builds tournent en parallèle. Sur 50 clients × 5 versions, vous lancez 250 builds par lots de 4 — prévoir une machine de build correcte (16 GB RAM, 8 cœurs, SSD).

L’ancien module community.docker.docker_image faisait tout (build, pull, push, tag) avec un argument source: pour basculer entre les modes. Depuis community.docker 4.0, il a été éclaté en modules dédiés, plus lisibles et plus robustes :

ActionModule 2026Ancien (déprécié)
Construire une imagedocker_image_builddocker_image: source: build
Pull depuis un registrydocker_image_pulldocker_image: source: pull
Push vers un registrydocker_image_pushdocker_image: source: pull + push: true
Tag d’une imagedocker_image_tagdocker_image: source: local + repository:

Migrer maintenant : les anciens modules sont en bugfix-only, plus de nouveaux features. Les playbooks qui utilisent docker_image: source: build continueront de marcher mais figeront leur capacité à exploiter les nouvelles options (BuildKit secrets, multi-stage selectif, etc.).

Si Docker n’est pas autorisé (RHEL durci, conformité ANSSI, CI sans daemon), la collection containers.podman offre les mêmes capacités en mode rootless :

Fenêtre de terminal
ansible-galaxy collection install containers.podman
- name: Build de l'image avec Podman (rootless)
containers.podman.podman_image:
name: "build-{{ client_name }}"
tag: "{{ target_version }}"
path: "{{ playbook_dir }}"
build:
args:
VERSION: "{{ target_version }}"
CLIENT: "{{ client_name }}"
- name: Lancer le conteneur de compilation
containers.podman.podman_container:
name: "build-{{ client_name }}-{{ target_version }}"
image: "build-{{ client_name }}:{{ target_version }}"
recreate: true
state: started
detach: false # idem Docker — attendre la fin
volumes:
- "{{ playbook_dir }}/ext:/ext:Z" # :Z pour SELinux
env:
VERSION: "{{ target_version }}"
CLIENT: "{{ client_name }}"

Différence clé : sur RHEL/AlmaLinux avec SELinux en enforcing, ajouter :Z au volume — sinon le conteneur reçoit Permission denied opaque. Voir Module SELinux.

Deux leviers contrôlent la concurrence :

LevierEffet
forks:ansible.cfg ou --forks NNombre max de hosts traités simultanément par le moteur Ansible
serial:au niveau du playNombre de hosts traités par vague dans ce play précis

Pour le pattern build-parallèle :

# ansible.cfg
[defaults]
forks = 8
playbook.yml
- hosts: builds
serial: 4 # 4 builds simultanés max, indépendamment de forks

serial: 4 prime sur forks: 8 — c’est la limite effective. Choisir serial: selon la RAM disponible, pas selon le nombre de cœurs : un build Docker peut consommer plusieurs GB pendant la phase compilation.

Si la compilation a besoin d’un token Git ou d’une clé d’API, ne pas le passer en ARG Dockerfile — il finit dans les couches de l’image et dans docker history.

BuildKit secrets (méthode propre) :

1.7
FROM alpine:3.20
RUN --mount=type=secret,id=git_token \
GIT_TOKEN=$(cat /run/secrets/git_token) \
&& git clone https://oauth2:${GIT_TOKEN}@gitlab.com/org/repo.git /src

Côté Ansible, le module docker_image_build ne supporte pas encore directement --secret BuildKit ; en attendant, deux options pragmatiques :

  1. Volume tmpfs monté pour le build, contenant le token, démonté immédiatement après.
  2. Conteneur intermédiaire qui fait le clone, puis COPY --from=clone /src /src dans l’image finale (le token reste dans le conteneur intermédiaire jeté).

À éviter absolument : args: { GIT_TOKEN: "{{ secret_token }}" }, même si le secret vient d’un Vault Ansible — la valeur finit en clair dans docker history.

SymptômeCauseFix
Tâches passent mais aucun artefact dans /extdetach: true — Ansible n’a pas attendudetach: false mandatory
Failed to import the required Python library (requests)venv Ansible sans requests/dockerpipx inject ansible requests docker
Conteneur précédent bloque le suivantPas de recreate: truerecreate: true ou nom unique par run
OOM killer pendant un build parallèleserial: trop haut pour la RAM dispoRéduire serial:, surveiller htop
Permission denied sur le volume /extSELinux + Podman:Z sur le volume
Secret en clair dans docker historyargs: { TOKEN: ... }BuildKit secrets ou conteneur intermédiaire
  • community.docker.docker_image_build + community.docker.docker_container : modules 2026, ne pas utiliser le legacy docker_image: source: build.
  • detach: false est la clé du pattern — Ansible attend la fin du conteneur.
  • recreate: true + nom unique par client/version = pas de pollution entre runs.
  • Inventaire groupé par client + variables → 1 host = 1 combinaison à builder.
  • serial: au play dimensionne le parallélisme RAM-conscient ; forks: côté config est le plafond global.
  • Alternative rootless : containers.podman avec :Z sur les volumes en SELinux enforcing.
  • Secrets : jamais en ARG. BuildKit secrets ou conteneur intermédiaire.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn