Aller au contenu
Infrastructure as Code medium

Bonnes pratiques Packer

27 min de lecture

logo packer

Ce guide vous apprend à structurer et maintenir des projets Packer professionnels. Vous saurez organiser vos fichiers, gérer les secrets de manière sécurisée, débugger les builds problématiques et automatiser tout le workflow avec votre CI/CD.

  • Organiser un projet Packer avec une structure claire et maintenable
  • Appliquer des conventions de nommage cohérentes
  • Gérer les secrets sans les exposer dans Git
  • Débugger efficacement les builds qui échouent
  • Intégrer Packer dans GitHub Actions et GitLab CI
  • Automatiser la validation et le formatage des templates

Prérequis : Maîtrise des templates HCL2 (guide HCL2), variables (guide variables), provisioners (guide provisioners) et post-processors (guide post-processors).

Un projet Packer qui démarre petit peut rapidement devenir complexe. Entre les différents environnements (dev, staging, prod), les multiples providers cloud et les secrets à gérer, le code devient vite difficile à maintenir sans conventions claires.

Les problèmes les plus courants que nous allons résoudre :

ProblèmeSymptômeSolution
Code monolithiqueUn seul fichier de 500+ lignesSéparation en fichiers par responsabilité
DuplicationCopier-coller de sources similairesVariables et locals pour factoriser
Secrets exposésMots de passe dans l’historique GitVariables sensibles + .gitignore strict
Builds non reproductibles”Ça marche sur ma machine”Versionning des plugins + CI/CD
Debugging difficileErreurs obscures sans contexteLogs détaillés + mode debug

Une bonne organisation facilite la maintenance et la collaboration. Voici la structure recommandée pour un projet Packer professionnel :

Structure de projet
my-packer-project/
├── packer.pkr.hcl # Configuration Packer (version + plugins)
├── docker.pkr.hcl # Sources Docker (ou aws.pkr.hcl, qemu.pkr.hcl...)
├── common.pkr.hcl # Variables communes
├── locals.pkr.hcl # Valeurs calculées
├── build.pkr.hcl # Définition des builds
├── variables/ # Fichiers de variables par environnement
│ ├── dev.auto.pkrvars.hcl # Valeurs dev (chargé automatiquement)
│ └── prod.pkrvars.hcl # Valeurs prod (chargé explicitement)
├── scripts/ # Scripts de provisioning
│ ├── setup-common.sh
│ └── validate.sh
├── files/ # Fichiers à uploader
├── output/ # Artefacts générés (gitignore)
├── .gitignore # Fichiers à ignorer
├── .github/workflows/ # CI/CD GitHub Actions
│ └── packer-build.yml
└── Makefile # Commandes simplifiées

Cette structure sépare clairement les responsabilités. Chaque fichier .pkr.hcl à la racine a un rôle précis, et Packer les charge tous automatiquement lors de l’exécution.

Ce fichier centralise la configuration Packer. Il déclare la version minimale requise et les plugins nécessaires. C’est le premier fichier lu par quiconque découvre le projet.

packer.pkr.hcl
packer {
# Version minimale de Packer requise
# Utiliser ">=" plutôt qu'une version exacte pour la flexibilité
required_version = ">= 1.10.0"
# Plugins requis avec leurs versions minimales
required_plugins {
docker = {
version = ">= 1.1.0"
source = "github.com/hashicorp/docker"
}
}
}

La contrainte >= 1.10.0 permet la compatibilité avec les versions futures tout en garantissant les fonctionnalités nécessaires. Pour les projets critiques, vous pouvez utiliser une contrainte plus stricte comme ~> 1.15.0 (compatible 1.15.x uniquement).

Regroupez les sources par type de provider. Cela facilite la maintenance et permet de réutiliser les définitions entre environnements.

docker.pkr.hcl
# Source de développement : image légère pour les tests
source "docker" "dev" {
image = "alpine:${var.alpine_version}"
commit = true
changes = [
"ENV APP_ENV=development",
"ENV APP_VERSION=${var.app_version}",
"LABEL org.opencontainers.image.version=${var.app_version}",
"LABEL org.opencontainers.image.environment=development"
]
}
# Source de production : image avec export pour distribution
source "docker" "prod" {
image = "alpine:${var.alpine_version}"
export_path = "${var.output_dir}/${var.app_name}-prod-${var.app_version}.tar"
}

Notez l’utilisation cohérente des variables. Les valeurs spécifiques (version Alpine, nom de l’app) viennent des variables, pas des constantes en dur.

Déclarez toutes les variables dans un fichier dédié avec descriptions et validations. C’est la documentation vivante de votre projet.

common.pkr.hcl
variable "app_name" {
type = string
description = "Nom de l'application (utilisé dans les tags et noms d'images)"
default = "mywebapp"
validation {
condition = can(regex("^[a-z][a-z0-9-]*$", var.app_name))
error_message = "Le nom doit commencer par une lettre minuscule et ne contenir que des caractères alphanumériques et des tirets."
}
}
variable "app_version" {
type = string
description = "Version de l'application (semver recommandé)"
default = "1.0.0"
validation {
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-z0-9]+)?$", var.app_version))
error_message = "La version doit suivre le format semver (ex: 1.2.3 ou 1.2.3-beta)."
}
}
variable "environment" {
type = string
description = "Environnement cible (dev, staging, prod)"
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être 'dev', 'staging' ou 'prod'."
}
}
# Variables sensibles
variable "registry_password" {
type = string
description = "Mot de passe du registry Docker (ne jamais commiter !)"
default = ""
sensitive = true
}

Les validations protègent contre les erreurs de saisie et documentent les contraintes attendues. L’attribut sensitive = true masque la valeur dans les logs.

Les locals centralisent les calculs et formatages dérivés des variables. Ils évitent la répétition et garantissent la cohérence.

locals.pkr.hcl
locals {
# Timestamp formaté pour les noms d'artefacts
timestamp = regex_replace(timestamp(), "[- TZ:]", "")
# Date courte pour les labels
build_date = formatdate("YYYY-MM-DD", timestamp())
# Version complète incluant le numéro de build CI/CD
full_version = var.build_number != "local" ? "${var.app_version}-${var.build_number}" : var.app_version
# Nom de base pour les artefacts
artifact_name = "${var.app_name}-${local.full_version}"
# Tags Docker
docker_tags = compact([
local.full_version,
var.app_version,
var.environment == "prod" ? "latest" : null,
"${var.environment}-latest"
])
}

La fonction compact() supprime les valeurs null de la liste — pratique pour les tags conditionnels comme latest qui ne s’applique qu’en production.

Des conventions cohérentes facilitent la lecture et la maintenance du code. Voici les règles recommandées :

ÉlémentConventionExemple
Variablessnake_caseapp_version, aws_region
Préfixe par domainedomain_nameapp_name, aws_instance_type
DescriptionsObligatoiresdescription = "..."
ÉlémentConventionExemple
Noms de sourcestype.nomdocker.dev, amazon-ebs.ubuntu
Noms de buildsDescriptifsname = "app-build"
Environnementsdev, staging, prodsource.docker.prod
ÉlémentConventionExemple
Templates.pkr.hclbuild.pkr.hcl
Variables auto.auto.pkrvars.hcldev.auto.pkrvars.hcl
Variables manuelles.pkrvars.hclprod.pkrvars.hcl
Scriptskebab-casesetup-common.sh

Les fichiers .auto.pkrvars.hcl sont chargés automatiquement par Packer. Utilisez-les pour les valeurs par défaut de développement. Les fichiers .pkrvars.hcl standards nécessitent -var-file= explicite.

La gestion des secrets est critique. Une clé API dans l’historique Git peut compromettre toute votre infrastructure. Voici les règles à suivre.

  1. Ne jamais commiter de secrets — Ni dans les fichiers, ni dans les messages de commit

  2. Utiliser sensitive = true — Marque la variable comme sensible (masquée dans les logs)

  3. Préférer les variables d’environnement — Plus sécurisé que les fichiers

  4. Maintenir un .gitignore strict — Bloquer tous les patterns de fichiers secrets

Packer reconnaît automatiquement les variables préfixées par PKR_VAR_ :

Définir des secrets avec des variables d'environnement
# Définir le secret (ne jamais le mettre dans un script commité)
export PKR_VAR_registry_password="mon-mot-de-passe-secret"
# Packer utilisera automatiquement cette valeur
packer build .

Pour les secrets complexes ou multiples, utilisez un fichier non commité :

Fichier de secrets (ajouté au .gitignore)
# Créer le fichier (jamais dans Git !)
cat > secrets.pkrvars.hcl <<EOF
registry_password = "secret1"
aws_secret_key = "secret2"
EOF
# Utiliser le fichier
packer build -var-file=secrets.pkrvars.hcl .

Voici un .gitignore complet pour les projets Packer :

.gitignore
# Artefacts de build
output/
*.tar
*.tar.gz
*.zip
*.ova
*.qcow2
# Fichiers Packer générés
packer_cache/
crash.log
manifest*.json
*.checksum
# Clés SSH temporaires (mode debug)
*.pem
# Variables sensibles - CRITIQUE !
*.secret.pkrvars.hcl
*-secret.pkrvars.hcl
secrets.pkrvars.hcl
# Environnements locaux
.env
.env.local
*.local.pkrvars.hcl

Les builds Packer peuvent échouer pour de nombreuses raisons : réseau, permissions, scripts, timeouts… Voici comment diagnostiquer efficacement.

Activez les logs détaillés avec la variable d’environnement PACKER_LOG :

Activer les logs détaillés
# Logs sur stderr
PACKER_LOG=1 packer build .
# Logs dans un fichier
PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build .

Les logs montrent chaque étape du build, les commandes exécutées et les réponses. Recherchez les lignes [ERROR] ou [WARN] pour identifier les problèmes.

Le mode debug (-debug) pause le build entre chaque étape, permettant d’inspecter l’état :

Build en mode debug
packer build -debug .

En mode debug, Packer :

  • Pause entre chaque étape (appuyez sur Entrée pour continuer)
  • Génère une clé SSH temporaire (fichier .pem) pour les builders cloud
  • Affiche les informations de connexion à l’instance

Cela permet de se connecter à l’instance en cours de build pour inspecter son état.

L’option -on-error contrôle le comportement en cas d’erreur :

Options on-error
# Demander quoi faire en cas d'erreur (recommandé)
packer build -on-error=ask .
# Nettoyer automatiquement (défaut)
packer build -on-error=cleanup .
# Garder l'instance pour investigation
packer build -on-error=abort .
# Réessayer l'étape échouée
packer build -on-error=run-cleanup-provisioner .

L’option ask est la plus pratique pour le développement : elle vous demande si vous voulez nettoyer, continuer ou abandonner.

ErreurCause probableSolution
timeout waiting for SSHInstance non accessibleVérifier security groups, key pair, réseau
script exited with non-zeroScript de provisioning échoueTester le script localement, vérifier les permissions
artifact could not be foundMauvais type d’artefact pour le post-processorVérifier commit vs export_path dans la source
too many open filesTrop de builders/pluginsAugmenter ulimit -n
plugin exited before connectChemin tmp trop longDéfinir TMPDIR vers un chemin court

Ajoutez toujours un provisioner de validation en fin de build pour détecter les problèmes avant de finaliser l’image :

Script de validation
provisioner "shell" {
scripts = ["scripts/validate.sh"]
}

Le script vérifie que tous les packages sont installés, les permissions correctes et les services configurés. S’il échoue, le build s’arrête avant de créer l’image défectueuse.

L’automatisation des builds Packer dans votre CI/CD garantit la reproductibilité et permet des déploiements fréquents.

Un pipeline CI/CD pour Packer suit généralement ces étapes :

  1. Validationpacker fmt -check et packer validate sur chaque PR
  2. Buildpacker build sur merge vers main
  3. Publication — Upload des artefacts vers un registry ou stockage
  4. Notification — Informer l’équipe du résultat

Voici un workflow GitHub Actions complet :

.github/workflows/packer-build.yml
name: Packer Build
on:
push:
branches: [main]
paths:
- "**.pkr.hcl"
- "**.pkrvars.hcl"
- "scripts/**"
pull_request:
branches: [main]
paths:
- "**.pkr.hcl"
env:
PACKER_VERSION: "1.15.0"
jobs:
# Job 1 : Validation (sur toutes les PRs)
validate:
name: Validate Packer Templates
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Packer
uses: hashicorp/setup-packer@v3
with:
version: ${{ env.PACKER_VERSION }}
- name: Initialize Packer
run: packer init .
- name: Format check
run: packer fmt -check -diff .
- name: Validate templates
run: packer validate .
# Job 2 : Build (uniquement sur main)
build:
name: Build Images
runs-on: ubuntu-latest
needs: validate
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Packer
uses: hashicorp/setup-packer@v3
with:
version: ${{ env.PACKER_VERSION }}
- name: Initialize Packer
run: packer init .
- name: Build images
env:
PKR_VAR_build_number: ${{ github.run_number }}
PKR_VAR_git_commit: ${{ github.sha }}
PKR_VAR_environment: prod
run: |
packer build \
-var-file=variables/prod.pkrvars.hcl \
-only="*.prod" \
.
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: packer-artifacts-${{ github.run_number }}
path: output/
retention-days: 30

Ce workflow :

  • Valide le format et la syntaxe sur chaque PR
  • Build uniquement sur merge vers main
  • Injecte le numéro de build et le commit SHA via variables d’environnement
  • Archive les artefacts pour 30 jours

Voici l’équivalent pour GitLab CI :

.gitlab-ci.yml
stages:
- validate
- build
- publish
variables:
PACKER_VERSION: "1.15.0"
image: hashicorp/packer:$PACKER_VERSION
.packer-init: &packer-init
before_script:
- packer version
- packer init .
# Validation sur toutes les MRs et la branche principale
validate:
stage: validate
<<: *packer-init
script:
- packer fmt -check -diff .
- packer validate .
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Build dev sur les branches non-main
build:dev:
stage: build
<<: *packer-init
script:
- mkdir -p output
- |
packer build \
-var "build_number=$CI_PIPELINE_ID" \
-var "git_commit=$CI_COMMIT_SHA" \
-var "environment=dev" \
-only="*.dev" \
.
artifacts:
paths:
- output/
expire_in: 1 week
rules:
- if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
# Build prod sur la branche principale
build:prod:
stage: build
<<: *packer-init
script:
- mkdir -p output
- |
packer build \
-var-file=variables/prod.pkrvars.hcl \
-var "build_number=$CI_PIPELINE_ID" \
-var "git_commit=$CI_COMMIT_SHA" \
-var "environment=prod" \
-only="*.prod" \
.
artifacts:
paths:
- output/
expire_in: 30 days
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Un Makefile simplifie les commandes courantes :

Makefile
.PHONY: help init fmt validate build clean
help: ## Affiche cette aide
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
init: ## Initialise Packer
packer init .
@mkdir -p output
fmt: ## Formate les fichiers HCL
packer fmt -recursive .
validate: init ## Valide les templates
packer validate .
build: validate ## Build toutes les images
packer build .
build-dev: validate ## Build l'image de développement
packer build -only="*.dev" .
build-prod: validate ## Build l'image de production
packer build -var-file=variables/prod.pkrvars.hcl -only="*.prod" .
debug: init ## Build en mode debug
PACKER_LOG=1 packer build -debug -only="*.dev" .
clean: ## Supprime les artefacts
rm -rf output/* manifest*.json *.checksum packer_cache/

Usage :

Fenêtre de terminal
make help # Voir les commandes disponibles
make validate # Valider les templates
make build-dev # Builder l'image dev

Packer peut construire plusieurs images en parallèle à partir du même template. C’est utile pour :

  • Créer des images pour plusieurs environnements (dev, staging, prod)
  • Supporter plusieurs providers cloud (AWS, Azure, GCP)
  • Générer des variantes (avec/sans outils de debug)

Listez plusieurs sources dans le bloc build :

Builds parallèles
build {
name = "app-build"
# Ces sources sont construites en parallèle
sources = [
"source.docker.dev",
"source.docker.staging",
"source.docker.prod"
]
# Provisioner commun à toutes les sources
provisioner "shell" {
scripts = ["scripts/setup-common.sh"]
}
# Provisioner spécifique au dev
provisioner "shell" {
only = ["docker.dev"]
inline = ["apk add --no-cache vim htop strace"]
}
# Provisioner spécifique à la prod
provisioner "shell" {
only = ["docker.prod"]
inline = ["rm -rf /var/cache/apk/* /tmp/*"]
}
}

L’attribut only restreint un provisioner à certaines sources. L’attribut inverse except exclut des sources spécifiques.

Utilisez -only ou -except pour filtrer les sources au moment du build :

Filtrer les builds
# Builder uniquement la source prod
packer build -only="*.prod" .
# Builder tout sauf dev
packer build -except="*.dev" .
# Syntaxe avec le nom complet
packer build -only="app-build.docker.prod" .

Les patterns acceptent des wildcards : *.prod matche docker.prod, amazon-ebs.prod, etc.

Les builds Packer peuvent être lents, surtout pour les images cloud. Voici quelques techniques d’optimisation.

Évitez de re-télécharger les packages à chaque build. Pour Alpine/apk :

Utiliser un miroir local ou cache
provisioner "shell" {
inline = [
"# Utiliser un miroir proche ou cache proxy",
"echo 'http://mirrors.example.com/alpine/v3.19/main' > /etc/apk/repositories",
"apk update && apk add --no-cache curl wget"
]
}

Pour les builds fréquents, créez une image de base avec les packages communs déjà installés :

Image de base
# Build 1 : Créer l'image de base (occasionnel)
source "docker" "base" {
image = "alpine:3.19"
commit = true
}
build {
name = "base-image"
sources = ["source.docker.base"]
provisioner "shell" {
inline = [
"apk update",
"apk add --no-cache curl wget ca-certificates"
]
}
}
# Build 2 : Utiliser l'image de base (fréquent)
source "docker" "app" {
image = "local/base:latest" # Image créée par le build précédent
commit = true
}

Passez les informations de build pour la traçabilité :

Variables CI/CD
packer build \
-var "build_number=${BUILD_NUMBER}" \
-var "git_commit=${GIT_COMMIT}" \
-var "build_date=$(date -Iseconds)" \
.

Ces valeurs apparaîtront dans les manifests et labels des images.

Causes possibles :

  • Différences de version Packer
  • Variables d’environnement manquantes
  • Permissions réseau différentes

Solutions :

  1. Verrouillez la version Packer dans la CI
  2. Utilisez packer version au début du pipeline
  3. Comparez les variables avec env | grep PKR

Solution : Utilisez un cache pour les plugins Packer :

GitHub Actions avec cache
- name: Cache Packer plugins
uses: actions/cache@v4
with:
path: ~/.config/packer/plugins
key: packer-plugins-${{ hashFiles('**/*.pkr.hcl') }}
- name: Initialize Packer
run: packer init .

Erreur : permission denied while trying to connect to the Docker daemon

Solution : En CI, utilisez Docker-in-Docker ou un runner avec accès Docker :

GitLab avec Docker-in-Docker
build:
image: hashicorp/packer:1.15.0
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2375
  • Structurez vos projets avec des fichiers séparés par responsabilité (sources, variables, builds)
  • Utilisez des conventions cohérentes pour les noms (snake_case pour variables, validation obligatoire)
  • Ne commitez jamais de secrets — variables d’environnement + .gitignore strict
  • Validez toujours avant de builderpacker fmt -check && packer validate
  • Debuggez avec PACKER_LOG=1, -debug et -on-error=ask
  • Automatisez en CI/CD — validation sur PR, build sur main, artefacts archivés
  • Ajoutez un provisioner de validation pour détecter les problèmes avant finalisation
  • Utilisez un Makefile pour simplifier les commandes de développement

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.