| FOTO | AUTO | EDV | AUDIO |

Packer: Golden Images

Packer baut aus einer Vorlage automatisch Maschinen-Images: Basis-Image starten, konfigurieren, prüfen, generalisieren, als neues Image speichern. Im IaC-Lab entsteht so das Image, aus dem jede Instanz des Scale Sets startet. Diese Seite vertieft, was im Lab nur angedeutet ist: Image-Hierarchien, Härtung, Tests, Schwachstellen-Scan, Nachweise und Builds für mehrere Plattformen.

Zurück zur IaC-Übersicht

Was beim Build passiert

 Basis-Image ──► temporäre Build-VM ──► Provisioner ──► Prüfungen ──► Generalisieren ──► neues Image
 (Marketplace       (von Packer            (Ansible,       (Tests,         (Host-Keys,        (Compute Gallery,
  oder eigenes)      angelegt)              Shell)          Scan)           machine-id, …)     Version x.y.z)
                                                                                      │
                                                     Build-VM und Hilfsressourcen ◄───┘ werden gelöscht

Das Ergebnis ist ein Golden Image: ein geprüfter, versionierter Stand, aus dem beliebig viele identische Instanzen entstehen. Patches und Änderungen führen zu einer neuen Version, nie zu Änderungen an laufenden Instanzen (siehe Immutable Infrastructure).

Bausteine

Block Zweck Beispiel
packer Packer-Version und benötigte Plugins required_plugins { azure … }
variable Eingabewerte, auch aus Umgebungsvariablen default = env(„ARM_SUBSCRIPTION_ID“)
locals Berechnete Werte Zeitstempel, zusammengesetzte Namen
source Plattform und Basis-Image (früher „Builder“) source „azure-arm“ „web“
build Verknüpft Quellen mit Provisionern sources = [„source.azure-arm.web“]
provisioner Konfiguriert die Build-VM shell, ansible, file
post-processor Verarbeitet das Ergebnis weiter manifest für Nachweise

Ein build kann mehrere source-Blöcke enthalten. Packer baut sie parallel – so entstehen aus einem Template Images für Azure und VMware (siehe unten).

Image-Hierarchie

Jedes Image vom Marketplace-Ubuntu aus komplett neu zu bauen, kostet Zeit und wiederholt die Härtung in jedem Template. Besser ist eine Hierarchie:

  Marketplace Ubuntu 24.04
          │
          ▼
  img-base-ubuntu2404        Härtung, Monitoring-Agent, Log-Forwarder, Zertifikate, Benutzerverwaltung
     │           │           verantwortet vom Plattform-Team, monatlich neu
     ▼           ▼
  img-web     img-app        nur rollenspezifische Pakete und Konfiguration
                             verantwortet vom jeweiligen Service-Team

Das Rollen-Image startet dann nicht vom Marketplace, sondern von einer Version des Basis-Images in der Compute Gallery:

source "azure-arm" "web" {
  use_azure_cli_auth = true
  subscription_id    = var.subscription_id
  location           = var.location
  vm_size            = "Standard_B2s"
  os_type            = "Linux"
 
  # Quelle: eigenes Basis-Image statt image_publisher/image_offer/image_sku
  shared_image_gallery {
    subscription   = var.subscription_id
    resource_group = "rg-iaclab-base"
    gallery_name   = "gal_iaclab"
    image_name     = "img-base-ubuntu2404"
    image_version  = var.base_image_version   # bewusst gepinnt, nicht "latest"
  }
 
  shared_image_gallery_destination {
    # ... wie im Lab
  }
}

Die Version des Basis-Images wird explizit angegeben. So ist nachvollziehbar, auf welchem Stand ein Rollen-Image aufsetzt, und ein neues Basis-Image rollt nicht unbemerkt in alle Rollen durch.

Härtung im Build

Die Härtung gehört ins Basis-Image. Sie wird einmal gebaut, einmal geprüft und steckt danach in jeder Instanz – ohne dass eine Instanz je ungehärtet laufen könnte.

Umsetzung mit Ansible: Für CIS-Benchmarks gibt es fertige Rollen, etwa aus dem Projekt Ansible-Lockdown (Rollen für RHEL, Ubuntu und weitere Distributionen). Vor dem Einsatz Reifegrad und unterstützte Benchmark-Version der jeweiligen Rolle prüfen und Regeln, die der Anwendung schaden, begründet deaktivieren – genau wie die Ausnahmen in der Checkov-Konfiguration.

base.yml
---
- name: Basis-Image – Härtung
  hosts: all
  become: true
  roles:
    - role: cis_hardening          # z. B. aus Ansible-Lockdown, im Repo gepinnt
      vars:
        # Beispiele für begründete Abweichungen:
        # SSH muss für Packer bis zum Ende des Builds erreichbar bleiben
        # Partitionierungsregeln greifen bei Cloud-Images nicht
  tasks:
    - name: Root-Login per SSH verbieten
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin no'

Nachweis mit OpenSCAP: Ob die Härtung wirkt, prüft ein unabhängiges Werkzeug. OpenSCAP mit den Inhalten des SCAP Security Guide bewertet das System gegen ein Profil und erzeugt einen HTML-Bericht, der als Build-Artefakt gespeichert wird. Für RHEL ist das der etablierte Weg; für Ubuntu hängt die Abdeckung von der eingesetzten SSG-Version ab.

# Beispiel RHEL 9 – im Build als Shell-Provisioner
oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \
  --report /tmp/openscap-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml || true

Das || true verhindert, dass einzelne nicht erfüllte Regeln den Build abbrechen. Die Bewertung erfolgt über den Bericht; wer strenger sein will, wertet die Ergebnisdatei aus und bricht bei bestimmten Regeln ab.

Tests im Build

Bevor ein Image in die Gallery kommt, muss es beweisen, dass es funktioniert. Ein einfaches Testskript läuft als Provisioner nach Ansible und vor der Generalisierung. Schlägt ein Test fehl, bricht der Build ab und es entsteht keine Image-Version.

web.sh
#!/usr/bin/env bash
# packer/tests/web.sh – Abnahmetests für das Web-Image
set -euo pipefail
 
fail() { echo "TEST FEHLGESCHLAGEN: $*" >&2; exit 1; }
 
systemctl is-enabled --quiet nginx         || fail "nginx ist nicht aktiviert"
systemctl is-active  --quiet nginx         || fail "nginx läuft nicht"
# Den Health-Endpunkt legt erst cloud-init an – für den Test kurz selbst erzeugen
echo ok > /var/www/html/health
trap 'rm -f /var/www/html/health' EXIT
curl -fsS http://localhost/health | grep -qx ok || fail "nginx liefert keine statischen Dateien aus"
test -s /etc/image-release                 || fail "/etc/image-release fehlt"
 
if dpkg -l unattended-upgrades 2>/dev/null | grep -q '^ii'; then
  fail "unattended-upgrades ist installiert"
fi
 
# Setzt die Härtung aus dem Basis-Image voraus
sshd -T | grep -qx 'permitrootlogin no'    || fail "Root-Login per SSH ist erlaubt"
 
echo "Alle Image-Tests bestanden."

Für umfangreichere Testsuiten eignen sich Werkzeuge wie Goss oder InSpec, die Erwartungen deklarativ beschreiben. Für den Anfang genügt ein Shell-Skript – Hauptsache, es wird getestet.

Schwachstellen-Scan und SBOM

Das Image wird vor der Freigabe auf bekannte Schwachstellen geprüft. Zusätzlich entsteht eine Software Bill of Materials (SBOM): eine maschinenlesbare Liste aller enthaltenen Pakete und Versionen. Wird später eine neue Schwachstelle bekannt, lässt sich ohne Zugriff auf eine einzige Instanz beantworten, welche Image-Versionen betroffen sind.

Das Werkzeug Trivy erledigt beides. Es wird nur für den Build in ein temporäres Verzeichnis installiert und danach wieder entfernt.

  # Scan und SBOM erzeugen
  provisioner "shell" {
    execute_command = "chmod +x {{ .Path }}; sudo sh -e '{{ .Path }}'"
    inline = [
      "curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /tmp/trivy",
      "/tmp/trivy/trivy rootfs --format cyclonedx --output /tmp/sbom.cdx.json /",
      "/tmp/trivy/trivy rootfs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --output /tmp/vuln-report.txt /",
    ]
  }
 
  # Berichte vor der Bewertung sichern – auch ein fehlschlagender Build liefert sie
  provisioner "file" {
    direction   = "download"
    source      = "/tmp/sbom.cdx.json"
    destination = "${path.root}/output/sbom-${var.image_version}.cdx.json"
  }
 
  provisioner "file" {
    direction   = "download"
    source      = "/tmp/vuln-report.txt"
    destination = "${path.root}/output/vuln-${var.image_version}.txt"
  }
 
  # Freigabe-Schwelle: behebbare kritische Schwachstellen stoppen den Build
  provisioner "shell" {
    execute_command = "chmod +x {{ .Path }}; sudo sh -e '{{ .Path }}'"
    inline = [
      "/tmp/trivy/trivy rootfs --scanners vuln --severity CRITICAL --ignore-unfixed --skip-db-update --exit-code 1 /",
      "rm -rf /tmp/trivy /tmp/sbom.cdx.json /tmp/vuln-report.txt /root/.cache/trivy",
    ]
  }

–ignore-unfixed blendet Schwachstellen aus, für die es noch keinen Patch gibt. Sie würden den Build dauerhaft blockieren, ohne dass man etwas tun könnte. Sie stehen aber weiterhin im Bericht und gehören in die Risikobewertung.

Das Verzeichnis packer/output/ muss vor dem Build existieren (mkdir -p packer/output); in der Pipeline wird es als Artefakt gespeichert.

Generalisierung

Ein Image darf nichts enthalten, was eine Instanz einzigartig macht. Sonst teilen sich später alle Instanzen dieselbe Identität.

Was Warum Wie
SSH-Host-Keys Gleiche Keys auf allen Instanzen = Man-in-the-Middle unbemerkbar waagent -deprovision bzw. rm /etc/ssh/ssh_host_*
/etc/machine-id Doppelte IDs stören systemd, Logging, DHCP cloud-init clean –machine-id
cloud-init-Status cloud-init muss auf jeder Instanz neu laufen cloud-init clean –logs –seed
Build-Benutzer Der temporäre Packer-Benutzer darf nicht bleiben waagent -deprovision+user
Logs, Shell-Historie Enthalten Build-Details, ggf. sensible Werte Logs leeren, HISTSIZE=0
Paket-Cache, Temp-Dateien Unnötige Größe, Reste von Scan-Werkzeugen apt-get clean, rm -rf /tmp/*

Für Azure bündelt der Azure Linux Agent (waagent) die meisten Schritte. Der letzte Provisioner im Build:

  provisioner "shell" {
    execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'"
    inline_shebang  = "/bin/sh -x"
    inline = [
      "apt-get clean",
      "rm -rf /tmp/* /var/tmp/*",
      "cloud-init clean --logs --seed --machine-id",
      "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync",
    ]
  }

deprovision+user löscht auch den Benutzer, über den Packer verbunden ist. Deshalb muss dieser Schritt der letzte sein.

Nachweise und Reproduzierbarkeit

Ein Image ist ein Release. Zu jeder Version gehört eine Dokumentation, die ohne Handarbeit entsteht:

Nachweis Quelle
Welcher Code? Git-Commit, im Manifest und als Tag am Image
Welches Basis-Image? Gepinnte Version im Template
Welche Pakete? SBOM (CycloneDX)
Welche Schwachstellen? Trivy-Bericht
Welche Härtung? OpenSCAP-Bericht
Tests bestanden? Build-Protokoll der Pipeline

Der Post-Processor manifest schreibt die Build-Ergebnisse in eine JSON-Datei:

variable "git_commit" {
  type    = string
  default = env("CI_COMMIT_SHA")
}
 
build {
  # ... sources und provisioner
 
  post-processor "manifest" {
    output     = "${path.root}/output/manifest.json"
    strip_path = true
    custom_data = {
      image_version = var.image_version
      git_commit    = var.git_commit
    }
  }
}

Dieselben Werte gehören als Tags an die Image-Version (azure_tags), damit sie auch in Azure und im Resource Graph sichtbar sind. In der Pipeline wird packer/output/ als Artefakt mit langer Aufbewahrung gespeichert – das ist der Release-Nachweis im Sinne von Release Management.

image:build:
  # ... wie in der Pipeline
  script:
    - mkdir -p packer/output
    # ... packer init / packer build
  artifacts:
    paths: [packer/output/]
    expire_in: 1 year
    when: always          # Berichte auch bei fehlgeschlagenem Build sichern

Mehrere Plattformen aus einem Template

Viele Umgebungen sind hybrid: Azure und VMware im eigenen Rechenzentrum. Mit Packer entstehen aus einem Template und einem Ansible-Playbook Images für beide Plattformen – mit identischer Härtung und identischem Softwarestand.

source "vsphere-clone" "web" {
  vcenter_server      = var.vcenter_server
  username            = var.vsphere_user
  password            = var.vsphere_password     # sensitive, aus Vault oder CI-Variable
  datacenter          = "DC1"
  cluster             = "Cluster01"
  datastore           = "ds-images"
  folder              = "Templates"
  template            = "tpl-ubuntu2404-base"    # vorhandene Basis-Vorlage
  vm_name             = "tpl-web-${var.image_version}"
  ssh_username        = "packer"
  ssh_private_key_file = var.ssh_private_key_file
  convert_to_template = true
}
 
build {
  sources = ["source.azure-arm.web", "source.vsphere-clone.web"]
 
  provisioner "ansible" {
    playbook_file = "${path.root}/../ansible/web.yml"
    # gilt für beide Plattformen
  }
 
  # Generalisierung plattformspezifisch
  provisioner "shell" {
    only   = ["azure-arm.web"]
    inline = ["sudo /usr/sbin/waagent -force -deprovision+user"]
  }
 
  provisioner "shell" {
    only   = ["vsphere-clone.web"]
    inline = [
      "sudo cloud-init clean --logs --seed --machine-id",
      "sudo rm -f /etc/ssh/ssh_host_*",
    ]
  }
}
packer build .                              # beide Plattformen parallel
packer build -only='azure-arm.web' .        # nur Azure

only und except an Provisionern steuern, welche Schritte für welche Plattform gelten. Das Beispiel ist bewusst verkürzt; ein vollständiger vSphere-Build braucht zusätzlich Netzwerk- und Hardware-Einstellungen sowie eine Basis-Vorlage mit cloud-init.

Befehle und Fehlersuche

Befehl Zweck
packer init . Plugins gemäß required_plugins installieren
packer fmt -recursive . Templates formatieren
packer validate -var image_version=0.0.0 . Template prüfen
packer inspect . Variablen, Quellen und Provisioner anzeigen
packer build -on-error=ask . Bei Fehler anhalten: Build-VM zur Untersuchung behalten
packer build -debug . Schritt für Schritt, mit Bestätigung vor jedem Schritt
PACKER_LOG=1 packer build . Ausführliches Protokoll

Häufige Probleme:

  • apt hängt oder meldet Sperre: cloud-init der Build-VM läuft noch. Abhilfe: als ersten Provisioner cloud-init status –wait.
  • Ansible erreicht die Build-VM nicht: Host-Key-Prüfung oder SSH-Proxy. Abhilfe: ANSIBLE_HOST_KEY_CHECKING=False und use_proxy = false.
  • Image-Version existiert bereits: Die Gallery überschreibt keine Versionen. Das ist gewollt – Versionsnummer erhöhen.
  • Build-VM bleibt nach Abbruch stehen: Packer räumt normalerweise auf; bei hartem Abbruch die temporäre Ressourcengruppe (pkr-Resource-Group-…) von Hand löschen.
  • Instanzen aus dem Image starten nicht sauber: Meist unvollständige Generalisierung. cloud-init-Log auf der Instanz prüfen (über Serial Console oder Boot-Diagnose).

Weiterführend

  • IaC-Lab – der Basis-Build im Zusammenhang
  • IaC-Pipeline – Image-Build in GitLab CI
  • Lebenszyklus – Versionsschema, monatliches Patch-Image, Aufräumen alter Versionen
  • cloud-init – was nach dem Image beim Start passiert
  • Ansible – die Konfiguration im Image