Anleitung: Container-Images testen
Diese Anleitung beschreibt, wie funktionale Smoke Tests für gehärtete Container-Images implementiert werden – und warum diese Tests eine notwendige Bedingung für automatisierte Image-Update-Prozesse sind.
Konzeptionelle Grundlage
Gehärtete Base-Images sollen gemäß der Container Hardening Checklist häufig – idealerweise automatisiert – aktualisiert werden: Sicherheitspatches, CVE-Behebungen und Versionsaktualisierungen der enthaltenen Laufzeitkomponenten erzeugen in kurzen Abständen neue Image-Tags. Vor jedem Release steht dabei i. d. R. ein menschlicher Review-Schritt. Damit diese Review fundiert und effizient erfolgen kann, benötigt es eine verlässliche Grundlage – automatisierte Smoke Tests unterstützen hier: Sie stellen sicher, dass das Image funktional intakt ist, bevor ein Mensch die finale Freigabe trifft.
Ohne funktionale Checks verliert der Review-Schritt an Aussagekraft:
- Stille Regression: Eine Breaking Change im Base-Image (z. B. ein entferntes Binary, eine geänderte Bibliothekssignatur, ein nicht-kompatibler Interpreter-Build) führt zu einem lauffähigen, aber dysfunktionalen Image – ohne dass dies in einer Review direkt sichtbar wäre.
- Fehlende Entscheidungsgrundlage: Der Reviewer kann funktionale Korrektheit nicht allein durch Inspektion der Changelogs oder Metadaten beurteilen.
Smoke Tests im CI-Testlauf schaffen diese Grundlage: Das neu gebaute Image wird als Laufzeitumgebung des Test-Jobs selbst verwendet. Schlägt ein Assert fehl, bricht die Pipeline ab – das Image erreicht den Review-Schritt gar nicht erst. Besteht der Test, kann der Reviewer fundierter freigeben, ohne eine vollständige Regressionstestebene vorhalten zu müssen.
Debug vs. Minimal
Häufig werden von einem Image zwei Varianten gebaut:
| Variante | Enthält | Zweck |
|---|---|---|
| minimal | Nur die Laufzeit | Produktionsbetrieb |
| debug | Laufzeit + statische Busybox (Shell) | Entwicklung, Tests, Diagnose |
Tests laufen im Debug-Image, weil CI-Runner eine Shell benötigen, um den Test-Befehl auszuführen. Das minimal-Image enthält keine Shell und kann daher nicht direkt als image: in einem CI-Job verwendet werden.
Übertragbarkeit auf das Minimal-Image
Bei Images, bei denen sich die beiden Varianten ausschließlich in der enthaltenen Shell und Tooling unterscheiden (wie etwa bei Node.js oder Python), gilt folgende Annahme: Besteht der Test im Debug-Image, ist das Minimal-Image funktional äquivalent. Der Laufzeitkern ist identisch – was getestet wird, ist der gemeinsame Anteil beider Varianten.
Teststruktur
Ein Smoke Test für ein gehärtetes Image beantwortet folgende Fragen:
- Ist die Laufzeit in der erwarteten Version vorhanden und funktionsfähig?
- Sind die Standardbibliotheken vollständig verfügbar?
- Sind plattformspezifische Funktionen (Kryptographie, Serialisierung, Debug-Schnittstellen) intakt?
Der Test verwendet keine externen Abhängigkeiten und kein Test-Framework – er nutzt ausschließlich die in der Laufzeit eingebauten Assertion-Mechanismen.
Implementierung
Das Grundprinzip ist laufzeitunabhängig: Der Test importiert Standardmodule, führt einfache Operationen aus und bricht bei Fehler mit einer aussagekräftigen Meldung ab. Die folgenden Beispiele illustrieren die Umsetzung für zwei konkrete Laufzeiten.
Beispiel: Node.js
Datei: test/test.js
const assert = require('assert')
const os = require('os')
const crypto = require('crypto')
console.log(`Node.js ${process.version}`)
console.log(`Platform: ${os.arch()}`)
// JSON roundtrip
const data = { status: 'ok', values: [1, 2, 3] }
assert.deepStrictEqual(
JSON.parse(JSON.stringify(data)),
data,
'json roundtrip failed',
)
// Basic arithmetic
const sum = Array.from({ length: 100 }, (_, i) => i + 1).reduce(
(a, b) => a + b,
0,
)
assert.strictEqual(sum, 5050, 'arithmetic failed')
// crypto module
const digest = crypto.createHash('sha256').update('test').digest('hex')
assert.strictEqual(digest.length, 64, 'crypto failed')
// v8 module is available (key debug tool)
const v8 = require('v8')
assert(typeof v8.getHeapStatistics === 'function', 'v8 not available')
// inspector module is available (used for debugging)
const inspector = require('inspector')
assert(typeof inspector.open === 'function', 'inspector not available')
console.log('All tests passed.')Ausführung im CI:
test_debug_image:
stage: test
parallel:
matrix:
- VERSION: ['20', '24']
image:
name: $CI_REGISTRY_IMAGE:${VERSION}-${CI_COMMIT_REF_SLUG}-amd64
pull_policy: always
script:
- node test/test.js
only:
- mainDer CI-Job verwendet das frisch gebaute Debug-Image als Ausführungsumgebung (image:). Das bedeutet: Der Test läuft innerhalb des zu prüfenden Images.
Empfehlungen für eigene Tests
Beim Erstellen von Tests für ein neues Image gilt:
Testen Sie das, was das Image bereitstellt. Bei Laufzeit-Images (z. B. Node.js, Python) ist das die Laufzeitumgebung selbst. Bei Dienst-Images (z. B. PostgreSQL, Cassandra) ist der Dienst das Testobjekt: Lässt er sich starten? Akzeptiert er Verbindungen? Reagiert er korrekt auf einfache Operationen? Der Test sollte immer die Funktion prüfen, die das Image im Betrieb erbringt.
Bleiben Sie minimal. Jede externe Abhängigkeit im Test ist ein potenzieller Fehlerpunkt, der nichts über die Image-Qualität aussagt. Nutzen Sie bevorzugt Werkzeuge, die das Image selbst mitbringt (Standardbibliotheken, eingebaute CLI-Tools).
Testen Sie alle Varianten im Matrix-Build. Ein defekter Build für eine spezifische Version darf nicht durch den erfolgreichen Build einer anderen Version maskiert werden.