Aller au contenu
Infrastructure as Code medium

Variables et fonctions Packer

28 min de lecture

logo packer

Ce guide vous apprend à paramétrer vos templates Packer avec les variables, locals et fonctions. Vous pourrez réutiliser le même template pour créer des images de développement, staging et production, simplement en changeant un fichier de configuration.

  • Déclarer des variables avec typage et validation
  • Créer des calculs intermédiaires avec les locals
  • Utiliser les fonctions HCL intégrées
  • Organiser vos variables avec les fichiers .pkrvars.hcl
  • Récupérer des données externes avec les data sources

Prérequis : Packer installé (guide d’installation), connaissance des templates HCL2 (syntaxe HCL2).

Imaginez que vous devez créer des images pour trois environnements : développement, staging et production. Sans variables, vous auriez trois templates presque identiques, avec juste quelques différences (packages installés, labels, configuration). Cette duplication pose plusieurs problèmes :

ProblèmeConséquence
Code dupliquéToute modification doit être répétée 3 fois
IncohérencesOubli de propager un changement
Maintenance difficilePlus de code = plus de bugs
Pas de traçabilitéImpossible de voir les différences rapidement

Avec les variables Packer, vous créez un seul template et vous passez les valeurs spécifiques à chaque environnement via des fichiers de configuration ou la ligne de commande. Le template devient une recette paramétrable.

Les variables d’entrée (ou input variables) sont les paramètres de votre template. Elles permettent de personnaliser le build sans modifier le code source. Cette section explique comment les déclarer, les typer et les utiliser.

Pour déclarer une variable, utilisez le bloc variable. Le nom de la variable doit être unique dans le template. Voici la syntaxe de base :

variable "image_name" {
type = string
default = "alpine"
description = "Nom de l'image de base à utiliser"
}

Chaque variable peut avoir plusieurs attributs. Le tableau suivant décrit les plus importants :

AttributObligatoireDescription
typeNonType de la valeur (string, number, bool, list, map, object)
defaultNonValeur par défaut (rend la variable optionnelle)
descriptionNonDocumentation de la variable
sensitiveNonSi true, la valeur est masquée dans les logs
validationNonRègles de validation personnalisées

Packer supporte plusieurs types de variables, des plus simples (chaînes, nombres) aux plus complexes (listes, maps, objets). Choisir le bon type permet à Packer de valider les valeurs et de détecter les erreurs avant le build.

Les types simples sont les briques de base. Voici comment les déclarer :

# Chaîne de caractères
variable "image_tag" {
type = string
default = "3.20"
description = "Tag de l'image Docker"
}
# Nombre (entier ou décimal)
variable "timeout_seconds" {
type = number
default = 300
description = "Timeout du build en secondes"
}
# Booléen
variable "enable_debug" {
type = bool
default = false
description = "Activer les logs de debug"
}

Les types complexes permettent de regrouper plusieurs valeurs. Utilisez-les pour les listes de packages, les configurations, ou les tags :

# Liste de chaînes
variable "packages_to_install" {
type = list(string)
default = ["curl", "jq", "vim"]
description = "Liste des paquets à installer"
}
# Map clé-valeur
variable "labels" {
type = map(string)
default = {
maintainer = "devops@example.com"
environment = "development"
}
description = "Labels à ajouter à l'image"
}
# Object avec structure définie
variable "instance_config" {
type = object({
size = string
count = number
public = bool
})
default = {
size = "small"
count = 1
public = false
}
description = "Configuration de l'instance"
}

La différence entre map et object est importante. Une map peut contenir n’importe quelles clés, mais toutes les valeurs doivent avoir le même type. Un object a une structure fixe avec des clés définies et potentiellement des types différents.

Une fois déclarée, une variable est accessible via la syntaxe var.nom_variable. Vous pouvez l’utiliser dans les blocs source, build, locals et dans d’autres expressions :

source "docker" "example" {
# Interpolation dans une chaîne
image = "${var.image_name}:${var.image_tag}"
# Utilisation directe d'un booléen
commit = var.enable_debug
# Transformation d'une map en liste
changes = [for k, v in var.labels : "LABEL ${k}=${v}"]
}
build {
sources = ["source.docker.example"]
provisioner "shell" {
# Jointure d'une liste
inline = ["apk add --no-cache ${join(" ", var.packages_to_install)}"]
}
}

Packer permet de définir des règles de validation personnalisées pour vos variables. Ces règles s’exécutent avant le build et arrêtent l’exécution si une valeur est invalide. C’est particulièrement utile pour détecter les erreurs tôt.

Chaque bloc validation contient deux attributs : condition (expression booléenne) et error_message (message affiché si la condition est fausse).

variable "environment" {
type = string
description = "Environnement cible (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être 'dev', 'staging' ou 'prod'."
}
}

Vous pouvez utiliser des expressions régulières pour valider le format d’une valeur. La fonction can() teste si une expression s’évalue sans erreur :

variable "project_name" {
type = string
description = "Nom du projet (alphanumériques et tirets)"
validation {
condition = length(var.project_name) >= 3 && length(var.project_name) <= 20
error_message = "Le nom doit contenir entre 3 et 20 caractères."
}
validation {
condition = can(regex("^[a-zA-Z][a-zA-Z0-9-]*$", var.project_name))
error_message = "Le nom doit commencer par une lettre."
}
}

Pour les tokens, mots de passe et autres secrets, utilisez l’attribut sensitive = true. Packer masquera la valeur dans les logs et la sortie de packer inspect :

variable "api_token" {
type = string
sensitive = true
description = "Token API (sera masqué dans les logs)"
}

Résultat de packer inspect :

> input-variables:
var.api_token: "<sensitive>"

Packer offre plusieurs méthodes pour assigner des valeurs aux variables. Cette section présente chaque méthode, de la plus simple à la plus structurée.

L’option -var permet de passer une valeur directement. Utilisez cette méthode pour les tests rapides ou les valeurs qui changent à chaque build :

Fenêtre de terminal
# Variable simple
packer build -var="image_tag=3.20" .
# Plusieurs variables
packer build -var="image_tag=3.20" -var="environment=prod" .
# Liste (syntaxe JSON)
packer build -var='packages=["curl","jq"]' .
# Map (syntaxe JSON)
packer build -var='labels={"env":"prod","team":"backend"}' .

Packer lit automatiquement les variables d’environnement préfixées par PKR_VAR_. Cette méthode est idéale pour les pipelines CI/CD où les secrets sont injectés via l’environnement :

Fenêtre de terminal
# Définir la variable
export PKR_VAR_api_token="mon-token-secret"
export PKR_VAR_environment="prod"
# Le build utilise automatiquement ces valeurs
packer build .

Pour les valeurs stables par environnement, utilisez des fichiers .pkrvars.hcl. Ces fichiers contiennent uniquement des assignations de valeurs :

dev.pkrvars.hcl
image_name = "alpine"
image_tag = "3.20"
environment = "dev"
packages = [
"curl",
"jq",
"vim",
"git"
]
labels = {
Team = "backend"
Project = "demo-app"
}

Utilisez -var-file pour charger le fichier :

Fenêtre de terminal
# Build avec configuration dev
packer build -var-file="dev.pkrvars.hcl" .
# Build avec configuration prod
packer build -var-file="prod.pkrvars.hcl" .

Les fichiers nommés *.auto.pkrvars.hcl sont chargés automatiquement sans avoir besoin de -var-file. Utilisez cette convention pour les valeurs par défaut de votre projet :

mon-projet/
├── build.pkr.hcl
├── variables.pkr.hcl
├── defaults.auto.pkrvars.hcl # Chargé automatiquement
└── prod.pkrvars.hcl # À charger explicitement

Quand une même variable est définie plusieurs fois, Packer utilise la dernière valeur rencontrée. Voici l’ordre de priorité, du plus faible au plus fort :

  1. Valeur default dans la déclaration (priorité la plus faible)
  2. Variables d’environnement PKR_VAR_*
  3. Fichiers *.auto.pkrvars.hcl (ordre alphabétique)
  4. Fichiers -var-file (ordre d’apparition)
  5. Options -var (priorité la plus forte)

Cette hiérarchie permet de définir des valeurs par défaut raisonnables, de les surcharger par environnement, et de les “forcer” ponctuellement via la ligne de commande.

Les variables locales, ou “locals”, sont des calculs intermédiaires que vous pouvez utiliser dans votre template. Contrairement aux variables d’entrée, elles ne sont pas paramétrables de l’extérieur : leur valeur est calculée à partir des variables d’entrée, d’autres locals, ou de fonctions.

Les locals résolvent plusieurs problèmes courants :

ProblèmeSolution avec locals
Expressions répétéesCalculer une fois, utiliser partout
Code illisibleNommer les calculs complexes
Maintenance difficileCentraliser la logique
Valeurs conditionnellesCalculer selon le contexte

Il existe deux syntaxes pour définir des locals. Le bloc locals (pluriel) permet de grouper plusieurs valeurs :

locals {
# Concaténation simple
full_image = "${var.base_image}:${var.image_tag}"
# Nom unique avec timestamp
build_id = "${var.project}-${formatdate("YYYYMMDD-hhmm", timestamp())}"
# Map de tags communs
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "packer"
BuildDate = timestamp()
}
# Sélection conditionnelle
packages = var.environment == "prod" ? ["curl", "ca-certificates"] : ["curl", "jq", "vim"]
}

Le bloc local (singulier) permet de définir une seule valeur avec l’option sensitive :

local "secret_hash" {
expression = md5("${var.project}-${var.environment}")
sensitive = true
}

Les locals sont accessibles via la syntaxe local.nom_local :

source "docker" "app" {
image = local.full_image
# Conversion de la map en labels Docker
changes = [for k, v in local.common_tags : "LABEL ${k}=${v}"]
}
build {
name = local.build_id
sources = ["source.docker.app"]
provisioner "shell" {
inline = ["apk add --no-cache ${join(" ", local.packages)}"]
}
}

Les locals peuvent référencer d’autres locals, mais attention aux cycles. Un local ne peut pas se référencer lui-même, ni référencer un local qui le référence (directement ou indirectement) :

locals {
# OK : a référence une variable
a = var.project
# OK : b référence a
b = "${local.a}-suffix"
# ERREUR : c référence d, et d référence c
# c = local.d
# d = local.c
}

Packer inclut de nombreuses fonctions intégrées pour manipuler les données. Ces fonctions sont identiques à celles de Terraform, ce qui facilite le transfert de connaissances entre les deux outils.

Les fonctions de chaînes manipulent le texte. Voici les plus utilisées :

locals {
# Conversion de casse
project_lower = lower("Mon-Projet") # "mon-projet"
project_upper = upper("mon-projet") # "MON-PROJET"
# Remplacement
slug = replace("mon projet", " ", "-") # "mon-projet"
# Sous-chaîne
prefix = substr("mon-projet", 0, 3) # "mon"
# Trim
cleaned = trimspace(" hello ") # "hello"
trim_prefix = trimprefix("v1.0.0", "v") # "1.0.0"
# Formatage (comme printf)
name = format("%s-build-%d", "app", 42) # "app-build-42"
# Jointure et split
tags = join(",", ["a", "b", "c"]) # "a,b,c"
parts = split(",", "a,b,c") # ["a", "b", "c"]
}

Depuis Packer 1.12, des fonctions supplémentaires sont disponibles :

locals {
# strcontains (v1.12+)
is_web = strcontains("web-server", "web") # true
# startswith / endswith (v1.13+)
is_v1 = startswith("v1.2.0", "v1") # true
is_tar = endswith("file.tar.gz", ".tar.gz") # true
}

Les fonctions de collections opèrent sur les listes et les maps :

locals {
liste = ["a", "b", "c", "a"]
config = { port = "8080", host = "0.0.0.0" }
# Longueur
count = length(local.liste) # 4
# Contient
has_b = contains(local.liste, "b") # true
# Concaténation de listes
merged = concat(["x"], local.liste) # ["x", "a", "b", "c", "a"]
# Valeurs uniques
unique = distinct(local.liste) # ["a", "b", "c"]
# Aplatir les listes imbriquées
flat = flatten([["a", "b"], ["c"]]) # ["a", "b", "c"]
# Clés et valeurs d'une map
keys = keys(local.config) # ["host", "port"]
vals = values(local.config) # ["0.0.0.0", "8080"]
# Fusion de maps
full = merge(local.config, { debug = "true" })
# Lookup avec valeur par défaut
port = lookup(local.config, "port", "3000") # "8080"
ssl = lookup(local.config, "ssl", "false") # "false"
# Coalesce : premier non-null, non-vide
result = coalesce("", null, "valeur") # "valeur"
}

Depuis Packer 1.12, vous disposez de fonctions de test sur les collections :

locals {
numbers = [1, 2, 3, 4, 5]
# alltrue : toutes les conditions vraies ?
all_positive = alltrue([for n in local.numbers : n > 0]) # true
# anytrue : au moins une condition vraie ?
has_five = anytrue([for n in local.numbers : n == 5]) # true
# sum (v1.13+)
total = sum(local.numbers) # 15
}

Les fonctions de dates manipulent les timestamps :

locals {
# Timestamp actuel (format RFC 3339)
now = timestamp() # "2026-02-14T09:30:00Z"
# Formatage personnalisé
date = formatdate("DD/MM/YYYY", timestamp()) # "14/02/2026"
time = formatdate("hh:mm:ss", timestamp()) # "09:30:00"
iso = formatdate("YYYY-MM-DD'T'HH:mm:ssZ", timestamp())
# Nom de fichier unique
filename = "build-${formatdate("YYYYMMDD-hhmm", timestamp())}.log"
}

Les codes de format sont inspirés de Go :

CodeSignificationExemple
YYYYAnnée 4 chiffres2026
MMMois 2 chiffres02
DDJour 2 chiffres14
hhHeure 24h09
mmMinutes30
ssSecondes00

Ces fonctions transforment les données pour le stockage ou la sécurité :

locals {
# Hashage
hash_md5 = md5("hello") # "5d41402abc4b2a76..."
hash_sha = sha256("hello") # "2cf24dba5fb0a30e..."
# UUID unique
id = uuidv4() # "a1b2c3d4-e5f6-..."
# Base64
encoded = base64encode("Hello World") # "SGVsbG8gV29ybGQ="
decoded = base64decode("SGVsbG8gV29ybGQ=") # "Hello World"
# JSON
json = jsonencode({ name = "app", version = "1.0" })
# URL encode
safe = urlencode("hello world") # "hello%20world"
}

Ces fonctions effectuent des calculs mathématiques :

locals {
# Min / Max
smallest = min(1, 5, 3) # 1
largest = max(1, 5, 3) # 5
# Valeur absolue
abs_val = abs(-5) # 5
# Arrondi
up = ceil(3.2) # 4
down = floor(3.8) # 3
}

Les data sources permettent de récupérer des informations externes pendant le build. Contrairement aux variables qui sont définies avant le build, les data sources sont évaluées au moment de l’exécution.

Les data sources sont utiles pour :

  • Récupérer l’ID de la dernière AMI disponible
  • Lire des secrets depuis un gestionnaire de secrets
  • Interroger une API pour obtenir une configuration

Un data source se déclare avec un bloc data. Le premier label est le type, le second est le nom :

data "http" "config" {
url = "https://api.example.com/config"
}

Les attributs de sortie sont accessibles via data.TYPE.NOM.attribut :

locals {
config_body = data.http.config.body
}

Voici un exemple qui récupère un fichier de configuration depuis une URL :

variable "config_url" {
type = string
default = "https://raw.githubusercontent.com/example/config/main/app.json"
description = "URL du fichier de configuration"
}
data "http" "app_config" {
url = var.config_url
request_headers = {
Accept = "application/json"
}
}
locals {
# Parser le JSON récupéré
config = jsondecode(data.http.app_config.body)
version = local.config.version
}

Mettons en pratique tous ces concepts avec un exemple réaliste. Ce template crée des images Docker paramétrables par environnement.

  1. Créez la structure du projet :

    Fenêtre de terminal
    mkdir -p packer-variables-demo && cd packer-variables-demo
  2. Créez le fichier variables.pkr.hcl avec les déclarations :

    variable "app_name" {
    type = string
    default = "myapp"
    description = "Nom de l'application"
    validation {
    condition = can(regex("^[a-z][a-z0-9-]*$", var.app_name))
    error_message = "Le nom doit être en minuscules."
    }
    }
    variable "app_version" {
    type = string
    default = "1.0.0"
    description = "Version (format semver)"
    validation {
    condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.app_version))
    error_message = "Format X.Y.Z requis."
    }
    }
    variable "environment" {
    type = string
    default = "dev"
    validation {
    condition = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environnement invalide."
    }
    }
    variable "packages" {
    type = map(list(string))
    default = {
    dev = ["curl", "jq", "vim", "git"]
    staging = ["curl", "jq", "git"]
    prod = ["curl", "ca-certificates"]
    }
    }
  3. Créez le fichier build.pkr.hcl avec la logique :

    packer {
    required_version = ">= 1.12.0"
    required_plugins {
    docker = {
    version = ">= 1.1.0"
    source = "github.com/hashicorp/docker"
    }
    }
    }
    locals {
    image_name = "${var.app_name}-${var.environment}"
    build_date = formatdate("YYYY-MM-DD", timestamp())
    build_id = substr(md5("${var.app_name}-${timestamp()}"), 0, 8)
    env_packages = lookup(var.packages, var.environment, var.packages["dev"])
    is_production = var.environment == "prod"
    labels = {
    "app.name" = var.app_name
    "app.version" = var.app_version
    "app.environment" = var.environment
    "app.build.date" = local.build_date
    }
    }
    source "docker" "app" {
    image = "alpine:3.20"
    commit = true
    changes = [for k, v in local.labels : "LABEL ${k}=${v}"]
    }
    build {
    name = local.image_name
    sources = ["source.docker.app"]
    provisioner "shell" {
    inline = [
    "echo 'Build: ${local.image_name} v${var.app_version}'",
    "echo 'ID: ${local.build_id}'",
    "echo 'Production: ${local.is_production}'",
    "apk add --no-cache ${join(" ", local.env_packages)}"
    ]
    }
    }
  4. Créez les fichiers de variables par environnement :

    dev.pkrvars.hcl
    app_name = "myapp"
    app_version = "1.0.0-dev"
    environment = "dev"
    prod.pkrvars.hcl
    app_name = "myapp"
    app_version = "1.0.0"
    environment = "prod"
  5. Testez les builds :

    Fenêtre de terminal
    packer init .
    packer validate -var-file=dev.pkrvars.hcl .
    packer build -var-file=dev.pkrvars.hcl .

Cette section liste les erreurs courantes liées aux variables et leurs solutions.

SymptômeCause probableSolution
variable "X" is not setVariable obligatoire sans valeurPassez une valeur via -var, PKR_VAR_* ou pkrvars
Invalid value for variableÉchec de validationVérifiez que la valeur respecte les règles
Cannot use local in data sourceLocal dans un data sourceUtilisez une variable à la place
Cycle detectedLocals qui se référencent mutuellementRestructurez les dépendances
Invalid variable typeType incorrect sur ligne de commandeUtilisez la syntaxe JSON pour les types complexes

Voici les points essentiels de ce guide sur les variables et fonctions Packer :

  • Variables d’entrée — Paramétrez vos templates avec variable {}, typez et validez
  • Fichiers pkrvars — Séparez les valeurs par environnement (dev, staging, prod)
  • Ordre de priorité — default < env < auto.pkrvars < -var-file < -var
  • Locals — Calculez des valeurs intermédiaires, évitez la répétition
  • Fonctions v1.12+strcontains, alltrue, anytrue simplifient les conditions
  • Fonctions v1.13+startswith, endswith, sum pour plus de flexibilité
  • Sensitive — Masquez les secrets dans les logs avec sensitive = true
  • Data sources — Récupérez des infos externes, mais utilisez var (pas local)

Maintenant que vous maîtrisez les variables, continuez avec ces guides :

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.