
Ce guide vous apprend à configurer et personnaliser vos images avec les provisioners Packer. Vous saurez installer des paquets, copier des fichiers, exécuter des scripts et utiliser Ansible pour automatiser la configuration de vos images machine.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Exécuter des scripts shell sur l’image en construction
- Transférer des fichiers vers l’image
- Lancer des commandes sur votre machine locale (pré/post build)
- Intégrer Ansible pour un provisioning avancé
- Combiner plusieurs provisioners de manière efficace
Prérequis : Packer installé (guide d’installation), connaissance des templates HCL2 (syntaxe HCL2) et des variables (variables et fonctions).
Qu’est-ce qu’un provisioner ?
Section intitulée « Qu’est-ce qu’un provisioner ? »Un provisioner est un bloc de configuration Packer qui modifie l’image après son démarrage initial. Si le builder crée la machine (VM ou conteneur), le provisioner la configure : installation de paquets, copie de fichiers, exécution de scripts, etc.
Pensez aux provisioners comme à un chef cuisinier qui reçoit les ingrédients (l’image de base fournie par le builder) et les transforme en plat fini (l’image finale configurée). Chaque provisioner est une étape de la recette.
Les provisioners s’exécutent dans l’ordre où ils apparaissent dans le bloc build. Cette séquentialité est importante : vous ne pouvez pas utiliser un fichier uploadé avant de l’avoir transféré, ni exécuter un script Python avant d’avoir installé Python.
| Provisioner | Exécution | Usage principal |
|---|---|---|
shell | Sur l’image distante | Scripts, installation de paquets |
file | Transfert vers l’image | Configuration, scripts, données |
shell-local | Sur la machine Packer | Préparation, génération, nettoyage |
ansible | Contrôle depuis Packer | Configuration complexe, idempotence |
breakpoint | Pause le build | Debugging interactif |
Provisioner shell
Section intitulée « Provisioner shell »Le provisioner shell est le plus utilisé. Il exécute des commandes ou scripts sur l’image en construction, via SSH (pour les VMs) ou exec (pour les conteneurs Docker). C’est l’équivalent de se connecter à la machine et de taper des commandes.
Commandes inline
Section intitulée « Commandes inline »La méthode la plus simple pour exécuter quelques commandes. Les commandes sont concaténées dans un script temporaire, uploadé vers l’image et exécuté. Elles partagent le même contexte shell, ce qui permet de définir des variables et de changer de répertoire.
build { sources = ["source.docker.alpine"]
# Provisioner shell avec commandes inline # Les commandes sont exécutées séquentiellement dans le même shell provisioner "shell" { inline = [ "echo '=== Début du provisioning ==='", "apk update", "apk add --no-cache curl jq vim", "echo '=== Vérification des installations ==='", "curl --version | head -1", "jq --version", "echo '=== Provisioning terminé ==='" ] }}Le tableau suivant décrit les options principales du provisioner shell :
| Option | Type | Description |
|---|---|---|
inline | liste de chaînes | Commandes à exécuter (mutuellement exclusif avec script) |
script | chaîne | Chemin vers un script à uploader et exécuter |
scripts | liste de chaînes | Plusieurs scripts, exécutés dans l’ordre |
environment_vars | liste de chaînes | Variables d’environnement au format KEY=value |
inline_shebang | chaîne | Shebang pour les commandes inline (défaut : /bin/sh -e) |
execute_command | chaîne | Commande d’exécution personnalisée |
valid_exit_codes | liste d’entiers | Codes de retour acceptés (défaut : [0]) |
Scripts externes
Section intitulée « Scripts externes »Pour des scripts plus complexes, séparez-les dans des fichiers. Cette approche améliore la lisibilité, permet la réutilisation et facilite les tests locaux. Le script est uploadé vers l’image puis exécuté.
Voici un exemple de template qui utilise des scripts externes :
variable "environment" { type = string default = "development" description = "Environnement cible (development, staging, production)"}
build { sources = ["source.docker.alpine"]
# Provisioner shell avec script externe # Le script est uploadé puis exécuté sur la cible provisioner "shell" { script = "scripts/setup.sh"
# Variables d'environnement passées au script environment_vars = [ "ENVIRONMENT=${var.environment}", "BUILD_DATE=${timestamp()}", "PACKER_BUILD_NAME=${build.name}" ] }
# Second provisioner avec plusieurs scripts # Les scripts sont exécutés dans l'ordre indiqué provisioner "shell" { scripts = [ "scripts/install-tools.sh", "scripts/cleanup.sh" ] }}Voici le script setup.sh utilisé. Notez l’utilisation du flag -e pour arrêter à la première erreur :
#!/bin/sh# Script de configuration initiale
set -e # Arrêter en cas d'erreur
echo "=== Configuration de l'environnement ==="echo "Environnement: ${ENVIRONMENT:-non défini}"echo "Date du build: ${BUILD_DATE:-non défini}"
# Mise à jour des paquetsapk update
# Configuration spécifique à l'environnementcase "${ENVIRONMENT}" in production) echo ">>> Configuration production..." ;; staging) echo ">>> Configuration staging..." ;; development) echo ">>> Configuration développement..." ;;esac
echo "=== Configuration terminée ==="Variables d’environnement Packer
Section intitulée « Variables d’environnement Packer »Packer injecte automatiquement certaines variables d’environnement dans chaque provisioner shell. Vous pouvez les utiliser dans vos scripts pour adapter le comportement selon le contexte :
| Variable | Description |
|---|---|
PACKER_BUILD_NAME | Nom du build Packer en cours |
PACKER_BUILDER_TYPE | Type du builder (ex: docker, qemu, amazon-ebs) |
PACKER_HTTP_ADDR | Adresse du serveur HTTP Packer (si activé) |
Ces variables sont utiles pour créer des scripts génériques qui s’adaptent au contexte. Par exemple, vous pourriez installer des outils de debugging uniquement pour les builds de développement.
Gestion des erreurs
Section intitulée « Gestion des erreurs »Par défaut, si une commande retourne un code différent de 0, le provisioner échoue et le build s’arrête. Ce comportement est voulu : une erreur silencieuse créerait une image incomplète ou défectueuse.
Pour les commandes qui peuvent légitimement échouer (comme grep qui retourne 1 si aucune correspondance), utilisez valid_exit_codes :
provisioner "shell" { inline = [ "grep 'pattern' /etc/config || true", # Méthode 1 : ignorer l'erreur ]
# Méthode 2 : déclarer les codes acceptés valid_exit_codes = [0, 1]}Gestion des redémarrages
Section intitulée « Gestion des redémarrages »Si votre provisioning nécessite un redémarrage (mise à jour du noyau, changement de configuration système), Packer sait gérer cette situation. Utilisez expect_disconnect et pause_before :
provisioner "shell" { expect_disconnect = true inline = [ "apt-get update && apt-get upgrade -y", "reboot" ]}
# Attendre que la machine redémarre avant de continuerprovisioner "shell" { pause_before = "30s" inline = [ "echo 'Machine redémarrée, on continue...'" ]}Provisioner file
Section intitulée « Provisioner file »Le provisioner file transfère des fichiers ou répertoires de votre machine locale vers l’image en construction. Il est essentiel pour déployer des fichiers de configuration, des scripts ou des données statiques.
Upload de fichiers
Section intitulée « Upload de fichiers »L’usage basique transfère un fichier unique. Le fichier source doit exister sur votre machine locale :
build { sources = ["source.docker.alpine"]
# Upload d'un fichier unique provisioner "file" { source = "files/config.json" destination = "/tmp/config.json" }
# Déplacer vers l'emplacement final avec les bonnes permissions # (le provisioner file upload vers l'utilisateur de connexion) provisioner "shell" { inline = [ "mkdir -p /etc/myapp", "mv /tmp/config.json /etc/myapp/config.json", "chmod 644 /etc/myapp/config.json" ] }}Upload de répertoires
Section intitulée « Upload de répertoires »Le provisioner file peut aussi transférer des répertoires entiers. Attention au slash final dans le chemin source — il change le comportement :
| Source | Destination | Résultat |
|---|---|---|
files/app | /app/ | Crée /app/app/ avec le contenu |
files/app/ | /app/ | Copie le contenu dans /app/ directement |
# SANS slash : copie le répertoire lui-mêmeprovisioner "file" { source = "files/app" # Pas de slash final destination = "/opt/" # Crée /opt/app/}
# AVEC slash : copie le contenu du répertoireprovisioner "file" { source = "files/app/" # Avec slash final destination = "/app/" # Copie le contenu dans /app/}Contenu dynamique
Section intitulée « Contenu dynamique »Au lieu d’uploader un fichier existant, vous pouvez générer le contenu dynamiquement avec l’attribut content. C’est utile pour injecter des variables de build :
variable "app_version" { type = string default = "1.0.0"}
provisioner "file" { content = jsonencode({ version = var.app_version buildDate = timestamp() builder = "packer" }) destination = "/app/build-info.json"}Fichiers générés pendant le build
Section intitulée « Fichiers générés pendant le build »Si un fichier est créé pendant le build (par exemple par un provisioner shell-local), il n’existe pas au moment de la validation. Utilisez l’attribut generated = true pour demander à Packer de ne vérifier l’existence qu’au moment de l’upload :
# Ce provisioner crée le fichier localementprovisioner "shell-local" { inline = [ "echo '{\"generated\": true}' > /tmp/dynamic-config.json" ]}
# Upload d'un fichier qui n'existait pas avant le buildprovisioner "file" { source = "/tmp/dynamic-config.json" destination = "/app/config.json" generated = true # Ne vérifie pas l'existence au validate}Téléchargement de fichiers
Section intitulée « Téléchargement de fichiers »Le provisioner file peut aussi télécharger des fichiers depuis l’image vers votre machine locale. Changez simplement la direction :
provisioner "file" { direction = "download" source = "/var/log/install.log" destination = "./build-logs/install.log"}Cette fonctionnalité est utile pour récupérer des logs de build, des artefacts générés ou des diagnostics.
Provisioner shell-local
Section intitulée « Provisioner shell-local »Le provisioner shell-local exécute des commandes sur votre machine locale (celle qui exécute Packer), pas sur l’image en construction. Il est utile pour :
- Préparer des fichiers avant de les uploader
- Générer des configurations dynamiques
- Nettoyer des fichiers temporaires après le build
- Notifier des systèmes externes (Slack, CI/CD)
Préparation avant le build
Section intitulée « Préparation avant le build »L’exemple suivant génère un fichier de métadonnées localement, puis l’uploade vers l’image :
locals { build_id = substr(uuidv4(), 0, 8) build_dir = "/tmp/packer-build-${local.build_id}"}
build { sources = ["source.docker.alpine"]
# Exécuté LOCALEMENT avant de provisionner l'image provisioner "shell-local" { inline = [ "echo '=== Préparation locale ==='", "echo 'Build ID: ${local.build_id}'", "mkdir -p ${local.build_dir}", "echo '{\"build_id\": \"${local.build_id}\"}' > ${local.build_dir}/metadata.json" ] }
# Upload du fichier généré provisioner "file" { source = "${local.build_dir}/metadata.json" destination = "/tmp/metadata.json" generated = true }
# Nettoyage local après le build provisioner "shell-local" { inline = [ "echo '=== Nettoyage local ==='", "rm -rf ${local.build_dir}" ] }}Filtrage par OS
Section intitulée « Filtrage par OS »Avec l’option only_on, vous pouvez exécuter des commandes uniquement sur certains systèmes d’exploitation (celui qui exécute Packer) :
# Commandes Linux/macOS uniquementprovisioner "shell-local" { only_on = ["linux", "darwin"] inline = [ "uname -a", "id" ]}
# Commandes Windows uniquementprovisioner "shell-local" { only_on = ["windows"] inline = [ "echo %USERNAME%" ]}Différences avec shell
Section intitulée « Différences avec shell »Le tableau suivant résume les différences entre shell et shell-local :
| Aspect | shell | shell-local |
|---|---|---|
| Où s’exécute | Sur l’image en construction | Sur la machine Packer |
| Accès aux fichiers | Fichiers de l’image | Fichiers locaux |
| Variables d’env | Passées via environment_vars | Environnement local |
| Usage typique | Configuration de l’image | Préparation/nettoyage |
| Communicateur | SSH, WinRM, exec | Aucun (local) |
Provisioner Ansible
Section intitulée « Provisioner Ansible »Le provisioner ansible permet d’utiliser des playbooks Ansible pour configurer l’image. Ansible est particulièrement adapté pour :
- Les configurations complexes et répétables
- L’idempotence (ré-exécution sans effet secondaire)
- La réutilisation de rôles existants
- La gestion de configuration avancée
Installation du plugin
Section intitulée « Installation du plugin »Le provisioner Ansible n’est pas inclus par défaut. Déclarez-le dans le bloc packer :
packer { required_version = ">= 1.12.0"
required_plugins { docker = { version = ">= 1.0.0" source = "github.com/hashicorp/docker" } ansible = { version = ">= 1.1.0" source = "github.com/hashicorp/ansible" } }}Exécutez packer init pour installer le plugin :
packer init template.pkr.hclConfiguration de base
Section intitulée « Configuration de base »Voici un exemple complet utilisant Ansible avec Docker :
variable "environment" { type = string default = "development"}
variable "app_version" { type = string default = "1.0.0"}
source "docker" "ubuntu" { image = "ubuntu:22.04" commit = true
run_command = [ "-d", "-i", "-t", "--name", "packer-ansible", "{{.Image}}", "/bin/bash" ]}
build { sources = ["source.docker.ubuntu"]
# Installer Python d'abord (prérequis Ansible) provisioner "shell" { inline = [ "apt-get update", "apt-get install -y python3 python3-apt", "ln -sf /usr/bin/python3 /usr/bin/python" ] }
# Provisioner Ansible provisioner "ansible" { playbook_file = "ansible/playbook.yml"
extra_arguments = [ "--extra-vars", "app_version=${var.app_version} environment=${var.environment}" ]
ansible_env_vars = [ "ANSIBLE_HOST_KEY_CHECKING=False", "ANSIBLE_NOCOLOR=True" ]
groups = ["webservers"] }}Options principales
Section intitulée « Options principales »Le provisioner Ansible offre de nombreuses options de configuration :
| Option | Description |
|---|---|
playbook_file | Chemin du playbook à exécuter (obligatoire) |
extra_arguments | Arguments supplémentaires pour ansible-playbook |
ansible_env_vars | Variables d’environnement pour Ansible |
groups | Groupes d’inventaire pour l’hôte |
user | Utilisateur Ansible (défaut : utilisateur Packer) |
galaxy_file | Fichier requirements.yml pour les rôles Galaxy |
use_proxy | Utiliser le proxy SSH Packer (défaut : true) |
Exemple de playbook
Section intitulée « Exemple de playbook »Voici un playbook compatible avec l’exemple précédent :
---- name: Configuration de l'image de base hosts: all become: true
vars: app_version: "1.0.0" environment: "development" packages: - curl - wget - jq - vim - git
tasks: - name: Afficher les informations du build ansible.builtin.debug: msg: "Version: {{ app_version }} - Environnement: {{ environment }}"
- name: Installation des paquets de base ansible.builtin.apt: name: "{{ packages }}" state: present update_cache: true
- name: Création du répertoire application ansible.builtin.file: path: /app state: directory mode: "0755"
- name: Création du fichier de version ansible.builtin.copy: content: | APP_VERSION={{ app_version }} ENVIRONMENT={{ environment }} dest: /app/version.env mode: "0644"Debugging
Section intitulée « Debugging »Si Ansible ne fonctionne pas comme prévu, activez le mode verbose :
provisioner "ansible" { playbook_file = "ansible/playbook.yml" extra_arguments = ["-vvvv"] # Mode très verbose}Options communes à tous les provisioners
Section intitulée « Options communes à tous les provisioners »Certaines options sont disponibles pour tous les provisioners. Elles permettent de contrôler l’exécution, gérer les erreurs et personnaliser le comportement.
Timing et retries
Section intitulée « Timing et retries »Ces options contrôlent le timing d’exécution et la gestion des erreurs :
provisioner "shell" { # Attendre 10 secondes avant d'exécuter pause_before = "10s"
# Timeout après 5 minutes timeout = "5m"
# Réessayer 3 fois en cas d'échec max_retries = 3
inline = ["echo 'Executed'"]}| Option | Type | Description |
|---|---|---|
pause_before | durée | Attente avant l’exécution |
timeout | durée | Temps maximum d’exécution |
max_retries | entier | Nombre de tentatives en cas d’échec |
Filtrage par builder
Section intitulée « Filtrage par builder »Avec only, vous pouvez limiter un provisioner à certains builders. C’est utile quand vous avez plusieurs sources :
source "docker" "alpine" { # ...}
source "qemu" "ubuntu" { # ...}
build { sources = ["source.docker.alpine", "source.qemu.ubuntu"]
# Uniquement pour Docker provisioner "shell" { only = ["docker.alpine"] inline = ["echo 'Docker only'"] }
# Uniquement pour QEMU provisioner "shell" { only = ["qemu.ubuntu"] inline = ["echo 'QEMU only'"] }
# Pour tous provisioner "shell" { inline = ["echo 'Both builders'"] }}Override par builder
Section intitulée « Override par builder »L’option override permet de personnaliser les options d’un provisioner selon le builder :
provisioner "shell" { inline = ["echo 'Default script'"]
override = { "docker.alpine" = { inline = ["apk add --no-cache curl"] } "qemu.ubuntu" = { inline = ["apt-get install -y curl"] } }}Ordre d’exécution et bonnes pratiques
Section intitulée « Ordre d’exécution et bonnes pratiques »Les provisioners s’exécutent dans l’ordre où ils apparaissent dans le bloc build. Cette séquentialité permet de construire une logique de provisioning cohérente.
Workflow recommandé
Section intitulée « Workflow recommandé »Un workflow de provisioning bien structuré suit généralement ces phases :
-
Préparation locale (
shell-local)Générer des fichiers de configuration, télécharger des dépendances, préparer l’environnement.
-
Upload des fichiers (
file)Transférer les configurations, scripts et données vers l’image.
-
Installation des dépendances (
shell)Mettre à jour le système et installer les paquets requis.
-
Configuration (
shellouansible)Configurer les services, créer les utilisateurs, appliquer les paramètres.
-
Validation (
shell)Vérifier que tout fonctionne : services démarrés, fichiers présents, permissions correctes.
-
Nettoyage (
shell)Supprimer les fichiers temporaires, vider les caches, réduire la taille de l’image.
-
Nettoyage local (
shell-local)Supprimer les fichiers temporaires sur la machine Packer.
Exemple complet
Section intitulée « Exemple complet »Voici un exemple qui suit ce workflow :
variable "environment" { type = string default = "development"}
variable "app_version" { type = string default = "1.0.0"}
locals { build_id = substr(uuidv4(), 0, 8) full_version = "${var.app_version}-${local.build_id}"
env_config = { development = { debug = true, log_level = "debug" } staging = { debug = false, log_level = "info" } production = { debug = false, log_level = "warn" } }
config = local.env_config[var.environment]}
source "docker" "alpine" { image = "alpine:3.19" commit = true}
build { name = "complete-demo" sources = ["source.docker.alpine"]
# Phase 1 : Préparation locale provisioner "shell-local" { inline = [ "mkdir -p /tmp/packer-${local.build_id}", "cat > /tmp/packer-${local.build_id}/config.json << 'EOF'", jsonencode({ version = local.full_version environment = var.environment debug = local.config.debug log_level = local.config.log_level }), "EOF" ] }
# Phase 2 : Installation des paquets provisioner "shell" { inline = [ "apk update", "apk add --no-cache curl jq vim git" ] }
# Phase 3 : Upload des fichiers provisioner "file" { source = "/tmp/packer-${local.build_id}/config.json" destination = "/tmp/config.json" generated = true }
# Phase 4 : Configuration provisioner "shell" { environment_vars = [ "APP_VERSION=${local.full_version}", "APP_ENV=${var.environment}" ] inline = [ "mkdir -p /app/config", "mv /tmp/config.json /app/config/", "cat > /app/.env << EOF", "APP_VERSION=$APP_VERSION", "APP_ENV=$APP_ENV", "EOF" ] }
# Phase 5 : Validation provisioner "shell" { inline = [ "echo '=== Validation ==='", "jq . /app/config/config.json", "cat /app/.env", "test -f /app/.env && echo 'OK: .env présent'" ] }
# Phase 6 : Nettoyage image provisioner "shell" { inline = [ "rm -rf /tmp/* /var/cache/apk/*" ] }
# Phase 7 : Nettoyage local provisioner "shell-local" { inline = [ "rm -rf /tmp/packer-${local.build_id}" ] }}Provisioner breakpoint
Section intitulée « Provisioner breakpoint »Pour debugger un build, utilisez le provisioner breakpoint. Il met en pause le build et attend que vous appuyiez sur Entrée pour continuer :
provisioner "shell" { inline = ["apt-get update"]}
# Pause ici pour inspecter l'étatprovisioner "breakpoint" { note = "Vérifiez que les paquets sont installés avant de continuer"}
provisioner "shell" { inline = ["apt-get install -y nginx"]}Pendant la pause, vous pouvez vous connecter à la machine (via SSH ou docker exec) pour inspecter son état.
Dépannage
Section intitulée « Dépannage »Cette section couvre les problèmes courants rencontrés avec les provisioners.
Script shell qui échoue silencieusement
Section intitulée « Script shell qui échoue silencieusement »Symptôme : Le build réussit mais l’image n’est pas configurée correctement.
Cause : Le script n’utilise pas set -e et les erreurs sont ignorées.
Solution : Ajoutez set -e au début de vos scripts ou utilisez inline_shebang = "/bin/sh -e".
Fichier non trouvé avec file provisioner
Section intitulée « Fichier non trouvé avec file provisioner »Symptôme : Erreur “Bad source” lors de la validation.
Cause : Le fichier n’existe pas ou le chemin est incorrect.
Solutions :
- Vérifiez que le fichier existe
- Utilisez
generated = truesi le fichier est créé pendant le build - Vérifiez le chemin relatif par rapport au répertoire d’exécution de Packer
Permission denied lors de l’upload
Section intitulée « Permission denied lors de l’upload »Symptôme : Erreur de permission lors de l’upload vers /etc/ ou /usr/.
Cause : Le provisioner file utilise l’utilisateur de connexion (souvent non-root).
Solution : Uploadez vers /tmp puis utilisez shell avec sudo pour déplacer le fichier.
Ansible “Gathering Facts” qui freeze
Section intitulée « Ansible “Gathering Facts” qui freeze »Symptôme : Le build reste bloqué sur “Gathering Facts”.
Cause : Problème de proxy SSH.
Solution : Ajoutez use_proxy = false dans le provisioner Ansible.
| Problème | Solution rapide |
|---|---|
| Script bash dans sh | inline_shebang = "/bin/bash -e" |
| Fichier généré non trouvé | generated = true |
| Permission denied | Upload vers /tmp puis sudo mv |
| Ansible freeze | use_proxy = false |
| Commande interactive | Ajouter -y ou équivalent |
À retenir
Section intitulée « À retenir »- Les provisioners configurent l’image après sa création par le builder
- shell exécute des commandes sur l’image distante — utilisez
set -epour capturer les erreurs - file transfère des fichiers — attention au slash final pour les répertoires
- shell-local exécute des commandes localement — idéal pour la préparation et le nettoyage
- ansible permet des configurations complexes et idempotentes — nécessite Python sur la cible
- Les provisioners s’exécutent dans l’ordre où ils apparaissent dans le template
- Utilisez
generated = truepour les fichiers créés pendant le build - Le workflow recommandé : préparation → upload → installation → configuration → validation → nettoyage