
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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).
Pourquoi les bonnes pratiques sont essentielles
Section intitulée « Pourquoi les bonnes pratiques sont essentielles »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ème | Symptôme | Solution |
|---|---|---|
| Code monolithique | Un seul fichier de 500+ lignes | Séparation en fichiers par responsabilité |
| Duplication | Copier-coller de sources similaires | Variables et locals pour factoriser |
| Secrets exposés | Mots de passe dans l’historique Git | Variables sensibles + .gitignore strict |
| Builds non reproductibles | ”Ça marche sur ma machine” | Versionning des plugins + CI/CD |
| Debugging difficile | Erreurs obscures sans contexte | Logs détaillés + mode debug |
Structure de projet recommandée
Section intitulée « Structure de projet recommandée »Une bonne organisation facilite la maintenance et la collaboration. Voici la structure recommandée pour un projet Packer professionnel :
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éesCette 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.
Fichier packer.pkr.hcl
Section intitulée « Fichier packer.pkr.hcl »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 { # 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).
Fichier de sources
Section intitulée « Fichier de sources »Regroupez les sources par type de provider. Cela facilite la maintenance et permet de réutiliser les définitions entre environnements.
# Source de développement : image légère pour les testssource "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 distributionsource "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.
Fichier de variables
Section intitulée « Fichier de variables »Déclarez toutes les variables dans un fichier dédié avec descriptions et validations. C’est la documentation vivante de votre projet.
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 sensiblesvariable "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.
Fichier locals
Section intitulée « Fichier locals »Les locals centralisent les calculs et formatages dérivés des variables. Ils évitent la répétition et garantissent la cohérence.
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.
Conventions de nommage
Section intitulée « Conventions de nommage »Des conventions cohérentes facilitent la lecture et la maintenance du code. Voici les règles recommandées :
Variables et locals
Section intitulée « Variables et locals »| Élément | Convention | Exemple |
|---|---|---|
| Variables | snake_case | app_version, aws_region |
| Préfixe par domaine | domain_name | app_name, aws_instance_type |
| Descriptions | Obligatoires | description = "..." |
Sources et builds
Section intitulée « Sources et builds »| Élément | Convention | Exemple |
|---|---|---|
| Noms de sources | type.nom | docker.dev, amazon-ebs.ubuntu |
| Noms de builds | Descriptifs | name = "app-build" |
| Environnements | dev, staging, prod | source.docker.prod |
Fichiers
Section intitulée « Fichiers »| Élément | Convention | Exemple |
|---|---|---|
| Templates | .pkr.hcl | build.pkr.hcl |
| Variables auto | .auto.pkrvars.hcl | dev.auto.pkrvars.hcl |
| Variables manuelles | .pkrvars.hcl | prod.pkrvars.hcl |
| Scripts | kebab-case | setup-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.
Gestion des secrets
Section intitulée « Gestion des secrets »La gestion des secrets est critique. Une clé API dans l’historique Git peut compromettre toute votre infrastructure. Voici les règles à suivre.
Règles fondamentales
Section intitulée « Règles fondamentales »-
Ne jamais commiter de secrets — Ni dans les fichiers, ni dans les messages de commit
-
Utiliser
sensitive = true— Marque la variable comme sensible (masquée dans les logs) -
Préférer les variables d’environnement — Plus sécurisé que les fichiers
-
Maintenir un .gitignore strict — Bloquer tous les patterns de fichiers secrets
Variables d’environnement
Section intitulée « Variables d’environnement »Packer reconnaît automatiquement les variables préfixées par PKR_VAR_ :
# 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 valeurpacker build .Pour les secrets complexes ou multiples, utilisez un fichier non commité :
# Créer le fichier (jamais dans Git !)cat > secrets.pkrvars.hcl <<EOFregistry_password = "secret1"aws_secret_key = "secret2"EOF
# Utiliser le fichierpacker build -var-file=secrets.pkrvars.hcl ..gitignore pour Packer
Section intitulée « .gitignore pour Packer »Voici un .gitignore complet pour les projets Packer :
# Artefacts de buildoutput/*.tar*.tar.gz*.zip*.ova*.qcow2
# Fichiers Packer généréspacker_cache/crash.logmanifest*.json*.checksum
# Clés SSH temporaires (mode debug)*.pem
# Variables sensibles - CRITIQUE !*.secret.pkrvars.hcl*-secret.pkrvars.hclsecrets.pkrvars.hcl
# Environnements locaux.env.env.local*.local.pkrvars.hclDebugging des builds
Section intitulée « Debugging des builds »Les builds Packer peuvent échouer pour de nombreuses raisons : réseau, permissions, scripts, timeouts… Voici comment diagnostiquer efficacement.
Logs détaillés
Section intitulée « Logs détaillés »Activez les logs détaillés avec la variable d’environnement PACKER_LOG :
# Logs sur stderrPACKER_LOG=1 packer build .
# Logs dans un fichierPACKER_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.
Mode debug
Section intitulée « Mode debug »Le mode debug (-debug) pause le build entre chaque étape, permettant d’inspecter l’état :
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.
Option on-error
Section intitulée « Option on-error »L’option -on-error contrôle le comportement en cas d’erreur :
# 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 investigationpacker build -on-error=abort .
# Réessayer l'étape échouéepacker 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.
Erreurs courantes et solutions
Section intitulée « Erreurs courantes et solutions »| Erreur | Cause probable | Solution |
|---|---|---|
timeout waiting for SSH | Instance non accessible | Vérifier security groups, key pair, réseau |
script exited with non-zero | Script de provisioning échoue | Tester le script localement, vérifier les permissions |
artifact could not be found | Mauvais type d’artefact pour le post-processor | Vérifier commit vs export_path dans la source |
too many open files | Trop de builders/plugins | Augmenter ulimit -n |
plugin exited before connect | Chemin tmp trop long | Définir TMPDIR vers un chemin court |
Provisioner de validation
Section intitulée « Provisioner de validation »Ajoutez toujours un provisioner de validation en fin de build pour détecter les problèmes avant de finaliser l’image :
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.
Intégration CI/CD
Section intitulée « Intégration CI/CD »L’automatisation des builds Packer dans votre CI/CD garantit la reproductibilité et permet des déploiements fréquents.
Workflow CI/CD typique
Section intitulée « Workflow CI/CD typique »Un pipeline CI/CD pour Packer suit généralement ces étapes :
- Validation —
packer fmt -checketpacker validatesur chaque PR - Build —
packer buildsur merge vers main - Publication — Upload des artefacts vers un registry ou stockage
- Notification — Informer l’équipe du résultat
GitHub Actions
Section intitulée « GitHub Actions »Voici un workflow GitHub Actions complet :
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: 30Ce 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
GitLab CI
Section intitulée « GitLab CI »Voici l’équivalent pour GitLab CI :
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 principalevalidate: 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-mainbuild: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 principalebuild: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_BRANCHMakefile pour le développement local
Section intitulée « Makefile pour le développement local »Un Makefile simplifie les commandes courantes :
.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 :
make help # Voir les commandes disponiblesmake validate # Valider les templatesmake build-dev # Builder l'image devBuilds parallèles
Section intitulée « Builds parallèles »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)
Configuration multi-sources
Section intitulée « Configuration multi-sources »Listez plusieurs sources dans le bloc build :
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.
Sélection à l’exécution
Section intitulée « Sélection à l’exécution »Utilisez -only ou -except pour filtrer les sources au moment du build :
# Builder uniquement la source prodpacker build -only="*.prod" .
# Builder tout sauf devpacker build -except="*.dev" .
# Syntaxe avec le nom completpacker build -only="app-build.docker.prod" .Les patterns acceptent des wildcards : *.prod matche docker.prod, amazon-ebs.prod, etc.
Optimisation des builds
Section intitulée « Optimisation des builds »Les builds Packer peuvent être lents, surtout pour les images cloud. Voici quelques techniques d’optimisation.
Cache des packages
Section intitulée « Cache des packages »Évitez de re-télécharger les packages à chaque build. Pour Alpine/apk :
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" ]}Images de base personnalisées
Section intitulée « Images de base personnalisées »Pour les builds fréquents, créez une image de base avec les packages communs déjà installés :
# 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}Variables de build CI/CD
Section intitulée « Variables de build CI/CD »Passez les informations de build pour la traçabilité :
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.
Dépannage CI/CD
Section intitulée « Dépannage CI/CD »Le build fonctionne localement mais pas en CI
Section intitulée « Le build fonctionne localement mais pas en CI »Causes possibles :
- Différences de version Packer
- Variables d’environnement manquantes
- Permissions réseau différentes
Solutions :
- Verrouillez la version Packer dans la CI
- Utilisez
packer versionau début du pipeline - Comparez les variables avec
env | grep PKR
Timeout lors du téléchargement des plugins
Section intitulée « Timeout lors du téléchargement des plugins »Solution : Utilisez un cache pour les plugins Packer :
- 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 .Erreurs de permissions Docker
Section intitulée « Erreurs de permissions Docker »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 :
build: image: hashicorp/packer:1.15.0 services: - docker:24-dind variables: DOCKER_HOST: tcp://docker:2375À retenir
Section intitulée « À retenir »- 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 builder —
packer fmt -check && packer validate - Debuggez avec
PACKER_LOG=1,-debuget-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