
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 sauvegardée localement pour les builders cloud
- Affiche les informations de connexion à l’instance (IP publique, IP privée, DNS)
Connexion SSH depuis le mode debug
Section intitulée « Connexion SSH depuis le mode debug »Lors de l’étape StepKeyPair, Packer crée une paire de clés temporaire et sauvegarde la clé privée dans le répertoire courant avec pour nom le label de la source (par exemple oapi_builder, packer_ec2, etc.) :
==> builder: Creating temporary keypair: packer_69d75ac3-887e-20d4-7419-85562764702a builder: Saving key for debug purposes: oapi_builderLe build est ensuite mis en pause à chaque étape. Une fois l’instance démarrée (après StepRunSourceVm), vous voyez l’IP publique et pouvez vous connecter :
# Packer affiche : Public IP: 217.75.166.255chmod 600 oapi_builderssh -i oapi_builder outscale@217.75.166.255# Adapter l'utilisateur selon la distribution : ubuntu, admin, rocky...Vous pouvez inspecter l’état de la VM, lire les logs, tester des commandes — puis appuyez sur Entrée dans le terminal Packer pour passer à l’étape suivante.
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 investigation (clé SSH générée si -debug est actif)packer 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 : Packer vous demande si vous voulez nettoyer, continuer ou abandonner. L’option abort arrête le build immédiatement sans détruire l’instance, ce qui est idéal pour diagnostiquer un provisioner qui échoue : combinez-la avec -debug pour avoir à la fois la clé SSH et l’instance préservée.
packer build -debug -on-error=abort .# → clé SSH disponible localement# → instance cloud préservée après l'erreur# → connexion possible pour inspecter manuellemementErreurs 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