Infrastructure Immuable : Créer ses propres images de serveurs « Golden Images » avec Packer et Cloud-Init

Pourquoi l’infrastructure immuable change vraiment la donne (et pourquoi « ça casse » souvent sans Golden Images)
Pendant longtemps, on a géré des serveurs comme on gère... des machines un peu vivantes. On se connecte, on patch, on installe un paquet, on modifie un fichier, on redémarre un service. Ça marche. Puis on recommence le mois suivant.
Sauf que ce modèle « mutable » a un défaut qui finit toujours par coûter cher : la dérive. La fameuse configuration drift. Deux serveurs censés être identiques ne le sont plus après quelques semaines. Un a un package en version N, l’autre en N-1. Un a un paramètre SSH changé « vite fait ». L’autre a un agent de monitoring qui a été réinstallé parce qu’il plantait. Et personne ne note tout, soyons honnêtes.
Résultat : des différences entre dev, staging et prod, des incidents du type « ça marchait hier », et des déploiements où on croise les doigts. Même quand on a de l’IaC autour, si la base des machines n’est pas cohérente, on traîne des surprises.
L’approche immuable vient casser cette habitude. L’idée est simple : on ne répare pas un serveur, on le remplace. On ne patch pas « à la main » un nœud, on rebuild une image, on redéploie. Donc le serveur devient jetable. Et surtout, reproductible.
C’est là que la notion de « Golden Image » entre en scène : une image de base préconfigurée, versionnée, traçable, et surtout reproductible. Une image qui sert de socle commun à toutes vos instances.
Dans cet article, on reste sur un périmètre clair :
- construire ses propres images avec Packer,
- et finaliser la personnalisation au boot avec cloud-init.
Promesse derrière tout ça : des déploiements plus rapides, auditables, et cohérents entre dev, staging et prod. Et moins de « mais pourtant sur mon serveur ça marche ».
Image d’or : ce qu’on met dedans (et ce qu’on évite)
Une Golden Image « saine », c’est un socle. Pas un serveur complet figé avec votre application, vos secrets, vos configs par environnement, et la moitié de /etc bricolée au fil du temps.
En gros, ce qui va bien dans l’image :
- OS + mises à jour de base (et éventuellement un modèle de patching par rebuild)
- durcissement minimal (SSH baseline, quelques sysctl, pare-feu de base si pertinent)
- Dépendances communes : Paquets standards, outils d’investigation (curl, jq, net-tools ou équivalent), time sync
- agents transverses : monitoring, logging, EDR, métriques, selon votre stack
- Baseline users si vous en avez une (souvent un compte d’admin break-glass, mais attention aux politiques internes)
- certificats racine, CA interne, configuration proxy si vous êtes en réseau contraint
- paramètres système communs : timezone, NTP, locale, limites, configuration kernel basique
Et ce qu’on évite, vraiment :
- secrets, clés privées, tokens cloud, kubeconfig, mots de passe
- configuration applicative spécifique (URL d’API prod, credentials, endpoints internes)
- tout ce qui varie par instance : hostname, IP, join à un cluster, config de rôle
La frontière pratique est simple :
- image = socle commun, stable
- cloud-init = personnalisation à l’instance
Cloud-init va très bien gérer : hostname, users et clés SSH, fichiers de config, montage de disques, bootstrap d’un service, join à un cluster, etc. Donc pas besoin de surcharger l’image.
Dernier point qui change la vie : le versioning.
- un nommage clair (app-os-version-date)
- un changelog, même minimal
- des tags immuables (git_sha, build_date, version)
- et idéalement un identifiant d’image que vous pouvez pinner côté Terraform
Les pièges classiques ?
- image trop lourde, qui met 25 minutes à builder et à démarrer
- image trop spécifique (une image par micro-service, puis vous perdez l’intérêt du socle)
- image pas testée, et vous répliquez le bug à grande échelle au prochain scale-out
Packer + cloud-init : la combinaison la plus simple pour industrialiser
Packer, c’est votre usine à images. Déclaratif, reproductible, et surtout automatisable en CI. Il sait builder pour plein de cibles : AWS AMI, Azure Managed Image, GCP Image, OpenStack, et aussi des plateformes plus « VM » comme Proxmox ou VMware selon les plugins.
cloud-init, c’est le standard de provisioning au premier boot. Vous donnez un user-data (souvent du YAML « cloud-config »), et cloud-init applique des modules dans un ordre déterminé : users, fichiers, commandes, services, etc.
Pourquoi les deux ensemble ?
- Packer « bake » le socle
- cloud-init applique la variabilité, sans transformer vos serveurs en objets modifiés à la main
Un flux type ressemble à ça, et franchement c’est propre :
- commit dans Git
- pipeline CI
- build Packer
- tests (smoke + checks)
- publication de l’image avec tags
- déploiement d’instances (Terraform, par exemple)
- cloud-init configure au boot
On peut supporter plusieurs plateformes, mais sans s’éparpiller : le modèle mental reste identique.
Architecture de référence : pipeline « Golden Image » prêt pour la prod
Imaginez un schéma logique, simple :
- un repo Git avec : packer + scripts + templates cloud-init
- un runner CI qui exécute packer
- un compte / projet cloud où les images sont publiées
- une stratégie de tags + promotion
Les étapes que je vois le plus souvent en prod, dans cet ordre :
packer fmt- build : packer build avec variables
- test : boot d’une instance à partir de l’image, smoke tests
- publication : tags + manifest + métadonnées
- promotion : dev vers prod (tag immuable, ou copie contrôlée selon le cloud)
Gestion des artefacts : ne vous contentez pas de « ça a buildé ».
- conservez l’ID de l’image
- conservez le manifest Packer
- gardez les logs de build
- si possible, sortez une SBOM (conceptuellement, au moins préparez l’espace pour)
Rollback : en immuable, c’est presque le point le plus important. Vous ne hotfixez pas à la main en prod. Vous redéployez l’image N-1. C’est tout.
Outils autour : Terraform est un bon compagnon pour consommer l’ID d’image. Ansible peut être utilisé, mais plutôt au moment du build Packer (ou pour validation). Évitez de remettre une grosse couche de configuration mutable post-déploiement, sinon vous recréez la dérive.
Pré-requis et conventions (avant d’écrire une ligne de Packer)
Avant le code, prenez 30 minutes pour décider du cadre.
Choisir une base OS
Ubuntu, Debian, RHEL, Alma, Rocky… le critère n’est pas « ce que j’aime », c’est :
- support éditeur et cycle de vie
- compatibilité cloud-init et qualité des images de base
- patching, sécurité, conformité interne
- disponibilité des paquets dont vous avez besoin
Pour un premier pipeline, une Ubuntu LTS est souvent le chemin le plus simple, car cloud-init est bien intégré et l’écosystème est large. Mais adaptez à votre contexte.
Conventions de nommage et tags
Décidez un format, et tenez-vous-y. Exemple :
golden-ubuntu-24-041.3.0git_shabuild_dateosos_versionteamenvironment=dev
Le point clé : pouvoir dire exactement « quelle image tourne où ».
Réseau et dépôts
- accès aux repos packages
- proxy si nécessaire
- miroirs internes si vous voulez du déterminisme et de la vitesse
Packer en HCL2
Utilisez HCL2. Lisible, variable-friendly, et c’est le standard actuel.
Structure de repo
Un exemple de structure qui évite le bazar :
repo/ packer/ ubuntu.pkr.hcl variables.pkr.hcl scripts/ base.sh hardening.sh cleanup.sh cloud-init/ base.yaml overlays/ dev.yaml prod.yaml tests/ smoke.sh docs/ CHANGELOG.md
Créer une Golden Image avec Packer (HCL) : exemple guidé de A à Z
On va montrer un exemple volontairement pragmatique : une image Ubuntu LTS sur AWS (AMI). Même si vous êtes sur Azure ou GCP, la logique reste la même. Vous remplacerez juste la source/builder.
Exemple Packer HCL (AWS AMI)
packer/ubuntu.pkr.hcl
hcl packer { required_version = ">= 1.10.0" required_plugins { amazon = { version = ">= 1.3.0" source = "github.com/hashicorp/amazon" } } }
variable "aws_region" { type = string default = "eu-west-3" }
variable "instance_type" { type = string default = "t3.micro" }
variable "ssh_username" { type = string default = "ubuntu" }
variable "ami_name_prefix" { type = string default = "golden-ubuntu-24-04" }
variable "version" { type = string default = "1.0.0" }
locals { build_date = formatdate("YYYYMMDD-hhmm", timestamp()) git_sha = coalesce(env("GIT_SHA"), "unknown") ami_name = "${var.ami_name_prefix}-${var.version}-${local.build_date}" }
source "amazon-ebs" "ubuntu" { region = var.aws_region instance_type = var.instance_type ssh_username = var.ssh_username
source_ami_filter { filters = { name = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" virtualization-type = "hvm" root-device-type = "ebs" } owners = ["099720109477"] # Canonical most_recent = true }
ami_name = local.ami_name ami_description = "Golden Image Ubuntu 24.04, version ${var.version}"
tags = { Name = local.ami_name version = var.version git_sha = local.git_sha build_date = local.build_date os = "ubuntu" os_version = "24.04" } }
build { name = "golden-ubuntu" sources = ["source.amazon-ebs.ubuntu"]
provisioner "shell" { scripts = [ "${path.root}/../scripts/base.sh", "${path.root}/../scripts/hardening.sh", "${path.root}/../scripts/cleanup.sh" ] }
post-processor "manifest" { output = "manifest.json" strip_path = true } }
Ça vous donne :
- une AMI nommée proprement
- des tags utiles
manifest.json
Scripts de provisioning (exemples)
scripts/base.sh
bash #!/usr/bin/env bash set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get -y upgrade
apt-get -y install
ca-certificates curl jq unzip
chrony
vim-tiny
systemctl enable chrony
scripts/hardening.sh
bash #!/usr/bin/env bash set -euxo pipefail
if [ -f /etc/ssh/sshd_config ]; then sed -i 's/^#?PermitRootLogin./PermitRootLogin no/' /etc/ssh/sshd_config || true sed -i 's/^#?PasswordAuthentication./PasswordAuthentication no/' /etc/ssh/sshd_config || true fi
cat >/etc/sysctl.d/99-golden.conf <<'EOF' net.ipv4.ip_forward=0 net.ipv4.conf.all.accept_redirects=0 net.ipv4.conf.all.send_redirects=0 EOF
sysctl --system || true
scripts/cleanup.sh
bash #!/usr/bin/env bash set -euxo pipefail
apt-get -y autoremove apt-get -y clean
rm -rf /var/lib/apt/lists/* rm -rf /tmp/* /var/tmp/*
rm -f /home/*/.bash_history || true rm -f /root/.bash_history || true
find /etc/ssh -name "ssh_host_key" -type f -maxdepth 1 -print || true
Note : selon votre cloud, vous ne devez pas supprimer certaines clés host SSH, ou au contraire les régénérer au premier boot. Décidez une règle et testez-la. Le « ça dépend » ici est réel.
Sortie : récupérer l’ID de l’image
Dans AWS, l’ID AMI est dans le manifest, dans les logs Packer, et peut aussi être exporté par votre CI. L’idée est de le passer ensuite à Terraform, et de le pinner. Pas de « latest » magique.
Provisioners : shell, Ansible, ou les deux ? (choisir sans se compliquer)
Shell :
- simple, rapide, pas de dépendance
- mais ça peut devenir illisible si vous empilez 800 lignes
Ansible :
- rôles réutilisables, plus structuré
- demande une discipline de versions et un runtime (ansible dans l’environnement CI)
Le mix est courant : shell pour bootstrap minimal, puis Ansible pour appliquer des rôles.
Recommandation pragmatique :
- commencez en shell tant que l’image fait peu de choses
- passez à Ansible si vous avez déjà des rôles éprouvés et une équipe à l’aise
Point clé, dans tous les cas : build déterministe.
- pinner les versions quand c’est nécessaire
latest- contrôler vos dépôts (miroir interne si besoin)
Sécurité dans l’image : durcissement minimal et hygiène de build
Dans l’image, vous voulez au minimum :
- OS à jour au moment du build
- baseline SSH cohérente
- hygiène de nettoyage
Sur les mises à jour automatiques : débat classique.
- soit vous activez unattended-upgrades
- soit vous assumez un modèle immuable strict : pas d’auto patch, on rebuild régulièrement
Les deux peuvent être valables. Mais mélangez-les consciemment. Pas par accident.
Et surtout, ne bakez jamais :
- secrets
- tokens
- clés privées
- credentials de registry
- fichiers de config contenant des mots de passe
Traçabilité :
- tags d’image
- manifest Packer
- et si vous voulez pousser plus loin, SBOM et scan vulnérabilités (on en reparle en conclusion)
cloud-init : personnaliser au boot sans casser l'immutabilité
Cloud-init fonctionne au premier boot via un user-data. Il exécute des modules dans un ordre précis, et vous pouvez lui demander de créer des users, injecter des clés SSH, écrire des fichiers avec permissions et owner, exécuter des commandes, activer et démarrer des services systemd, et configurer des montages.
Ce que vous faites avec cloud-init, en général
- Hostname
- Users et accès SSH
- Configuration applicative locale : fichiers, variables, templates simples
- Bootstrap d'un agent ou d'un service
- Join à un cluster : Kubernetes worker, Consul, Nomad, etc.
Points d'attention qui font mal quand on débute
- Indentation YAML incorrecte
- Taille max du user-data selon le cloud provider
- Échec silencieux de cloud-init faute de consultation des logs
Séparation recommandée
- Un cloud-init « base » commun à toutes les instances
- Des overlays par environnement (dev, stage, prod) si besoin
Exemple de fichier cloud-config : users, SSH, fichiers et services
cloud-init/base.yaml
yaml #cloud-config hostname: web-01 manage_etc_hosts: true
users:
- name: deploy groups: [ sudo ] shell: /bin/bash sudo: [ "ALL=(ALL) NOPASSWD:ALL" ] ssh_authorized_keys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... votre_cle_publique"
write_files:
- path: /etc/myapp/config.env owner: root:root permissions: "0640" content: | APP_ENV=prod LOG_LEVEL=info API_ENDPOINT=https://api.example.internal
runcmd:
- [ bash, -lc, "systemctl daemon-reload" ]
- [ bash, -lc, "systemctl enable myapp || true" ]
- [ bash, -lc, "systemctl restart myapp || true" ]
Ce fichier illustre trois aspects essentiels : la gestion des users avec leurs clés SSH, l'écriture de fichiers avec permissions précises, et l'exécution de commandes de bootstrap.
Deux remarques importantes :
- Évitez d'y mettre des secrets en clair. Si vous avez besoin de secrets au boot, passez par un secret manager et récupérez-les via un mécanisme d'identité (IAM role, workload identity, etc.)
/var/log/cloud-init.log/var/log/cloud-init-output.log
Tester une Golden Image comme un produit (sinon vous déplacez juste les incidents)
Une image cassée, ce n’est pas « un serveur cassé ». C’est un incident reproductible à l’infini.
Tests minimaux, déjà très utiles :
- boot OK
- SSH OK
- time sync OK
- agents attendus démarrés
- ports de base ouverts ou fermés comme prévu
- espace disque et FS corrects
- cloud-init appliqué sans erreurs
Approche plus robuste :
- tests automatisés type Testinfra / Serverspec (même concept, peu importe l’outil)
- validation de conformité « CIS-lite » si vous avez des exigences
- tests cloud-init explicites : fichiers présents, services démarrés, logs propres
Critères de promotion en prod :
- artefact validé
- ID stable
- changelog rempli, même succinct
- possibilité de rollback immédiat (N-1)
Intégration CI/CD : construire, tagger, publier, promouvoir
Un pipeline type (GitHub Actions, GitLab CI, Jenkins, etc.) :
packer fmt+packer validate- build Packer
- lancer une instance de test et exécuter des smoke tests
- publier l’image, sauvegarder manifest + logs
- promotion dev vers prod
Gestion des credentials :
- privilégiez OIDC et des rôles temporaires
- évitez les clés longues durées dans les variables CI
Promotion :
- soit vous gardez la même image et vous changez des tags logiques
- soit vous copiez vers un autre compte/projet (souvent le cas en entreprise)
Côté déploiement : Terraform consomme l’ID d’image. Et vous le pinnez. C’est la base de la reproductibilité.
Erreurs classiques (et comment les éviter) quand on démarre avec Packer et cloud-init
- Confondre provisioning et configuration : trop dans cloud-init, ou trop dans l’image. Gardez l’image comme socle.
- Ne pas pinner les versions : un build aujourd’hui, un build demain, deux résultats différents. Parfois ça passe. Jusqu’au jour où non.
- cloud-init qui échoue silencieusement : YAML invalide, module qui plante, et vous ne lisez pas les logs.
- Images non testées : vous découvrez le problème quand vous scalez en prod.
- Pas de rollback : vous hotfixez à la main, et vous revenez au monde mutable sans le dire.
Conclusion : un process simple à maintenir (et comment passer à l’étape suivante)
Si on résume sans fioritures :
- une Golden Image, c’est le socle stable
- cloud-init, c’est la variabilité contrôlée au boot
- Packer, c’est la reproductibilité et l’industrialisation
Le conseil le plus rentable : commencez petit. Une image. Un pipeline. Des smoke tests. Un versioning propre. Et déjà, vous allez sentir la différence.
Ensuite vous pourrez enrichir :
- durcissement plus strict
- signature / attestation d’artefacts
- SBOM et scans de vulnérabilités
- rotation régulière des images
- images par rôle (base, runtime, data, etc.) si ça fait sens
L’idée clé à garder en tête, celle qui change vraiment la façon d’opérer : en immuable, on corrige en rebuildant, pas en patchant à la main. Et oui, au début ça pique un peu. Puis après, c’est difficile de revenir en arrière.
Questions fréquemment posées
Qu'est-ce que l'infrastructure immuable et pourquoi est-elle préférable à l'infrastructure mutable ?
L'infrastructure immuable consiste à ne pas modifier un serveur en production, mais à le remplacer entièrement par une nouvelle image préconfigurée. Contrairement à l'approche mutable où les serveurs sont patchés ou modifiés manuellement, ce modèle évite la dérive de configuration, garantit la cohérence entre environnements et réduit les incidents liés aux différences non documentées.
Quelle est la définition d'une Golden Image et que doit-elle contenir ?
Une Golden Image est une image de base stable, versionnée et traçable qui sert de socle commun à toutes les instances. Elle contient le système d'exploitation avec mises à jour, un durcissement minimal (SSH baseline, sysctl), dépendances communes, agents transverses (monitoring, logging), utilisateurs basiques, certificats racine et paramètres système communs comme timezone ou NTP.
Quels éléments faut-il éviter d'inclure dans une Golden Image ?
Il faut éviter d'inclure des secrets (clés privées, tokens), des configurations applicatives spécifiques (URL d'API, credentials), ainsi que tout ce qui varie par instance comme hostname, IP ou configurations de rôle. Ces personnalisations doivent être gérées au boot via cloud-init pour garantir la flexibilité et la sécurité.
Comment fonctionne la personnalisation des instances avec cloud-init ?
Cloud-init est un outil standard de provisioning au premier démarrage. Il utilise un fichier user-data en YAML appelé cloud-config pour appliquer des modules dans un ordre défini : création d'utilisateurs, configuration réseau, montage de disques, bootstrap de services ou intégration dans un cluster. Cela permet de finaliser la personnalisation sans modifier l'image elle-même.
Quels sont les avantages de combiner Packer avec cloud-init pour construire des images ?
Packer permet de construire des images déclaratives, reproductibles et automatisables en CI pour différentes plateformes (AWS AMI, Azure Managed Image, GCP Image...). Associé à cloud-init qui gère la personnalisation au boot, cette combinaison industrialise la création d'images fiables et cohérentes facilitant les déploiements rapides et auditables.
Quels pièges classiques faut-il éviter lors de la création d'une Golden Image ?
Il faut éviter que l'image soit trop lourde (longue à builder/démarrer), trop spécifique (une image par micro-service perdant le socle commun) ou non testée (réplication massive de bugs). Un bon versioning clair avec changelog et tags immuables est essentiel pour gérer efficacement les images.
0 Commentaires