Best Practices für die Containererstellung

In diesem Artikel wird eine Reihe von Best Practices zum Erstellen von Containern beschrieben. Diese Vorgehensweisen eignen sich für eine Vielzahl von Aufgabenstellungen, von der Verkürzung der Erstellungszeit bis zum Erstellen kleinerer und stabilerer Images, um Container einfacher anlegen (z. B. mit Cloud Build) und in Google Kubernetes Engine (GKE) ausführen zu können.

Einige der hier beschriebenen Best Practices sind wichtiger als andere. Bei der Ausführung einer Produktionsarbeitslast können Sie z. B. auf einige davon verzichten, andere sind jedoch von grundlegender Bedeutung. Insbesondere ist die Bedeutung der Best Practices für die Sicherheit subjektiv. Ob Sie sie umsetzen, hängt von Ihrer Umgebung und den jeweiligen Einschränkungen ab.

Sie benötigen Kenntnisse über Docker und Kubernetes, um die Informationen in diesem Artikel optimal nutzen zu können. Einige der hier beschriebenen Best Practices gelten auch für Windows-Container, aber bei den meisten wird davon ausgegangen, dass Sie mit Linux-Containern arbeiten. Hinweise zur Ausführung und zum Betrieb von Containern finden Sie unter Best Practices für den Betrieb von Containern.

Einzelne Anwendung pro Container verpacken

Wichtigkeit: HOCH

Beim Einstieg in die Arbeit mit Containern werden diese oft fälschlicherweise wie virtuelle Maschinen behandelt, die viele verschiedene Aufgaben gleichzeitig ausführen. Ein Container kann auf diese Weise funktionieren, aber dann werden die meisten Vorteile des Containermodells nicht optimal genutzt. Denken Sie zum Beispiel an einen klassischen Apache-/MySQL-/PHP-Stapel: Man könnte versucht sein, alle Komponenten in einem einzigen Container auszuführen. Die Best Practice besteht allerdings darin, zwei oder drei verschiedene Container zu verwenden: einen für Apache, einen für MySQL und ggf. einen für PHP, wenn PHP-FPM ausgeführt wird.

Da der Lebenszyklus des Containers auf den Lebenszyklus der darin gehosteten Anwendung abgestimmt ist, sollte jeder Container nur eine Anwendung enthalten. Beim Start eines Containers sollte auch die Anwendung gestartet werden und beim Beenden der Anwendung sollte gleichzeitig auch der Container geschlossen werden. Im folgenden Diagramm ist diese Best Practice veranschaulicht.

Diagramm, das den Bootvorgang ohne benutzerdefiniertes Image zeigt

Abbildung 1. Beim Container auf der linken Seite wird der Best Practice gefolgt, beim Container auf der rechten Seite nicht.

Wenn sich mehrere Anwendungen in einem Container befinden, können diese unterschiedliche Lebenszyklen oder verschiedene Status haben. So kann es z. B. passieren, dass der Container zwar ausgeführt wird, eine der Kernkomponenten darin aber abgestürzt ist oder nicht mehr reagiert. Ohne eine zusätzliche Diagnose kann das übergeordnete Containerverwaltungssystem (Docker oder Kubernetes) nicht feststellen, ob der Container fehlerfrei ist. Im Fall von Kubernetes bedeutet das, dass Kubernetes den Container nicht automatisch neu startet, wenn eine Kernkomponente nicht reagiert.

Bei öffentlichen Images können Sie eventuell die folgenden Aktionen beobachten, die Sie jedoch nicht nachmachen sollten:

Ordnungsgemäßer Umgang mit PID 1, Signalverarbeitung und Zombie-Prozessen

Wichtigkeit: HOCH

Linux-Signale sind die Hauptmethode zur Steuerung des Lebenszyklus von Prozessen in einem Container. In Einklang mit der vorangegangenen Best Practice sollten Sie dafür sorgen, dass Linux-Signale von der Anwendung ordnungsgemäß verarbeitet werden. So lässt sich gewährleisten, dass ihr Lebenszyklus genau mit dem Container abgestimmt werden kann, in dem sie sich befindet. Das wichtigste Linux-Signal ist SIGTERM, weil damit ein Prozess beendet wird. Die Anwendung kann auch ein SIGKILL-Signal empfangen, mit dem der Prozess nicht ordnungsgemäß beendet wird, oder ein SIGINT-Signal, das bei der Eingabe von Ctrl+C gesendet und in der Regel wie SIGTERM behandelt wird.

Prozess-IDs (PIDs) sind eindeutige Kennzeichnungen, die der Linux-Kernel jedem Prozess zuweist. PIDs werden mit einem Namespace versehen, d. h., ein Container hat eine eigene Gruppe von PIDs, die PIDs auf dem Hostsystem zugeordnet sind. Der erste Prozess, der beim Start eines Linux-Kernels geöffnet wird, hat die PID 1. Bei einem normalen Betriebssystem ist dieser Prozess das Init-System, z. B. "systemd" oder "SysV". Analog dazu erhält der erste in einem Container gestartete Prozess ebenfalls die PID 1. Docker und Kubernetes verwenden Signale, um mit den Prozessen in Containern zu kommunizieren, insbesondere um sie zu beenden. Sowohl Docker als auch Kubernetes können nur Signale an den Prozess senden, der in einem Container die PID 1 hat.

Im Zusammenhang mit Containern verursachen PIDs und Linux-Signale zwei Probleme, die berücksichtigt werden müssen.

Problem 1: Umgang des Linux-Kernels mit Signalen

Der Linux-Kernel behandelt Signale für den Prozess mit PID 1 anders als Signale für andere Prozesse. Für diesen Prozess werden nicht automatisch Signal-Handler registriert, d. h., Signale wie SIGTERM oder SIGINT bleiben standardmäßig ohne Wirkung. Standardmäßig müssen Sie Prozesse mithilfe von SIGKILL beenden, womit ein ordnungsgemäßes Herunterfahren verhindert wird. Je nach Anwendung kann es durch die Verwendung von SIGKILL zu Fehlern kommen, die den Nutzer direkt betreffen, sowie zu unterbrochenen Schreibvorgängen (für Datenspeicher) oder unerwünschten Benachrichtigungen im Überwachungssystem.

Problem 2: Umgang der klassischen Init-Systeme mit verwaisten Prozessen

Klassische Init-Systeme wie "systemd" werden u. a. zum Entfernen (Ernten) verwaister Zombie-Prozesse verwendet. Verwaiste Prozesse, also Prozesse, deren übergeordnete Elemente nicht mehr existieren, werden wieder an den Prozess mit PID 1 angehängt, der sie "erntet", sobald sie "absterben". In einem normalen Init-System funktioniert das so. Aber in einem Container ist der jeweilige Prozess mit der PID 1 dafür zuständig. Wenn dieser Prozess den Vorgang des Erntens nicht ordnungsgemäß verarbeitet, besteht das Risiko, dass plötzlich nicht mehr genügend Arbeitsspeicher oder andere Ressourcen zur Verfügung stehen.

Für diese Probleme gibt es mehrere gängige Lösungen, die in den folgenden Abschnitten beschrieben werden.

Lösung 1: Als PID 1 ausführen und Signal-Handler registrieren

Diese Lösung ist nur für das erste Problem bestimmt. Sie ist geeignet, wenn von der Anwendung auf kontrollierte Weise untergeordnete Prozesse erzeugt werden (was häufig der Fall ist), sodass das zweite Problem vermieden wird.

Der einfachste Weg, diese Lösung umzusetzen, besteht darin, den Prozess im Dockerfile mit den Anweisungen CMD und/oder ENTRYPOINT zu starten. Im folgenden Dockerfile ist z. B. nginx der erste und einzige Prozess, der gestartet wird.

FROM debian:9

RUN apt-get update && \
    apt-get install -y nginx

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

Manchmal müssen Sie möglicherweise die Umgebung im Container vorbereiten, damit der Prozess ordnungsgemäß ausgeführt werden kann. In diesem Fall besteht die Best Practice darin, den Container beim Start ein Shell-Skript aufrufen zu lassen. Aufgabe dieses Shell-Skripts ist es, die Umgebung vorzubereiten und den Hauptprozess zu starten. Wenn Sie nach diesem Ansatz verfahren, hat allerdings das Shell-Skript die PID 1, nicht der Prozess. Aus diesem Grund müssen Sie mit dem integrierten Befehl exec dafür sorgen, dass der Prozess aus dem Shell-Skript heraus gestartet wird. Mit dem Befehl exec wird das Skript durch das gewünschte Programm ersetzt. Der Prozess übernimmt dann PID 1.

Lösung 2: Prozess-Namespace-Freigabe in Kubernetes aktivieren

Wenn Sie die Prozess-Namespace-Freigabe für einen Pod aktivieren, verwendet Kubernetes für alle Container in diesem Pod den gleichen Prozess-Namespace. Der Kubernetes-Pod-Infrastrukturcontainer übernimmt dann PID 1 und erntet automatisch verwaiste Prozesse.

Lösung 3: Spezialisiertes Init-System verwenden

Wie in einer klassischen Linux-Umgebung können Sie auch ein Init-System zur Lösung dieser Probleme verwenden. Allerdings sind herkömmliche Init-Systeme wie "systemd" oder "SysV" zu komplex und zu groß, um ausschließlich für diesen Zweck genutzt zu werden. Deshalb empfehlen wir die Verwendung eines Init-Systems wie tini, das speziell für Container entwickelt wurde.

Wenn Sie ein spezielles Init-System nutzen, hat der Init-Prozess die PID 1 und übernimmt folgende Aufgaben:

  • Registrierung der korrekten Signal-Handler
  • Sicherstellung der Funktion von Signalen für die Anwendung
  • Ernten etwaiger Zombie-Prozesse

In Docker können Sie diese Lösung sofort nutzen. Verwenden Sie dazu die Option --init des Befehls docker run. Wenn Sie diese Lösung in Kubernetes verwenden möchten, müssen Sie das Init-System im Container-Image installieren und als Einstiegspunkt für den Container verwenden.

Docker-Build-Cache optimieren

Wichtigkeit: HOCH

Mit dem Docker-Build-Cache kann das Erstellen von Container-Images beschleunigt werden. Images werden Schicht für Schicht erstellt. Mit jeder Anweisung im Dockerfile kommt im Image eine neue Schicht hinzu. Während eines Build-Vorgangs werden Schichten von Docker möglichst aus einem früheren Build-Vorgang wiederverwendet und damit möglicherweise kostenintensive Schritte übersprungen. Der Build-Cache von Docker kann nur verwendet werden, wenn er für alle vorangegangenen Build-Schritte verwendet wurde. In der Regel ist dieses Verhalten äußerst nützlich, weil es Build-Vorgänge beschleunigt. Es sind jedoch ein paar Dinge zu beachten.

Damit Sie z. B. in vollem Umfang vom Docker-Build-Cache profitieren können, müssen Sie die Build-Schritte, die sich häufig ändern, unten im Dockerfile platzieren. Wenn Sie sie oben platzieren, kann der Build-Cache von Docker nicht für die anderen Build-Schritte verwendet werden, die sich weniger häufig ändern. Da in der Regel für jede neue Version Ihres Quellcodes ein neues Docker-Image erstellt wird, sollten Sie den Quellcode im Dockerfile möglichst spät dem Image hinzufügen. Im folgenden Diagramm ist ersichtlich, dass Docker bei Änderung von STEP 1 nur die Schichten aus dem Schritt FROM debian:9 wiederverwenden kann. Wenn Sie dagegen STEP 3 ändern, kann Docker die Schichten für STEP 1 und STEP 2 wiederverwenden.

Beispiele für die Verwendung des Docker-Build-Caches

Abbildung 2. Beispiele für die Verwendung des Docker-Build-Caches. Die wiederverwendbaren Schichten sind grün dargestellt, die neu zu erstellenden Schichten rot.

Die Wiederverwendung von Schichten hat noch eine weitere Folge: Wenn für einen Build-Schritt ein im lokalen Dateisystem gespeicherter Cache benötigt wird, muss dieser Cache im selben Build-Schritt erstellt werden. Wenn dieser Cache nicht erstellt wird, wird der Build-Schritt möglicherweise mit einem veralteten Cache ausgeführt, der aus einem früheren Build-Vorgang stammt. Dieses Verhalten tritt häufig bei Paketmanagern wie "apt" oder "yum" auf: Sie müssen Ihre Repositories mit demselben RUN-Befehl wie Ihre Paketinstallation aktualisieren.

Wenn Sie im folgenden Dockerfile den zweiten RUN-Schritt ändern, wird der Befehl apt-get update nicht noch einmal ausgeführt. Die Folge ist ein veralteter apt-Cache.

FROM debian:9

RUN apt-get update
RUN apt-get install -y nginx

Kombinieren Sie stattdessen die beiden Befehle zu einem einzigen RUN-Schritt:

FROM debian:9

RUN apt-get update && \
    apt-get install -y nginx

Unnötige Tools entfernen

Wichtigkeit: MITTEL

Zum Schutz Ihrer Anwendungen vor Angreifern sollten Sie versuchen, ihre Angriffsfläche zu verkleinern. Entfernen Sie dazu alle nicht benötigten Tools. Entfernen Sie z. B. Dienstprogramme wie netcat, mit dem auf Ihrem System eine Reverse-Shell erstellt werden kann. Wenn netcat nicht im Container vorhanden ist, müssen sich Angreifer einen anderen Weg suchen.

Diese Best Practice gilt für jede Arbeitslast, auch wenn sie nicht in einem Container ausgeführt wird. Der Unterschied besteht darin, dass die Implementierung mit Containern viel einfacher als mit klassischen virtuellen Maschinen oder Bare-Metal-Servern ist.

Einige dieser Tools können der Fehlerbehebung dienen. Wenn Sie diese Best Practice weit genug einhalten, werden umfassende Logs sowie die Systeme zum Tracing und Profilieren fast immer obligatorisch. Dies hat zur Folge, dass Sie sich nicht mehr auf lokale Fehlerbehebungstools verlassen können, da für diese häufig umfangreiche Berechtigungen erforderlich sind.

Inhalt des Dateisystems

Der erste Teil dieser Best Practice betrifft den Inhalt des Container-Images. Legen Sie möglichst wenig Inhalte in einem Image ab. Wenn Sie die Anwendung zu einer einzigen statisch verknüpften Binärdatei kompilieren können und diese Binärdatei in das Scratch-Image aufnehmen, enthält das endgültige Image nur die Anwendung, sonst nichts. Wenn Sie die Anzahl der im Image verpackten Tools reduzieren, schränken Sie die möglichen Aktionen ein, die ein potenzieller Angreifer im Container ausführen kann. Weitere Informationen finden Sie unter Möglichst kleines Image erstellen.

Sicherheit des Dateisystems

Es reicht nicht aus, dass sich im Image keine Tools befinden: Sie müssen verhindern, dass potenzielle Angreifer ihre eigenen Tools installieren können. Dazu können Sie zwei Methoden kombinieren:

  • Vermeiden Sie im Container die Ausführung als Root: Auf diese Weise erhalten Sie eine erste Sicherheitsschicht, mit der z. B. verhindert werden kann, dass Angreifer mithilfe eines im Image eingebetteten Paketmanagers wie apt-get oder apk Root-Dateien verändern. Damit diese Methode funktioniert, müssen Sie den Befehl sudo deaktivieren oder deinstallieren. Dieses Thema wird im Abschnitt Ausführung als Root vermeiden ausführlicher behandelt.

  • Starten Sie den Container im schreibgeschützten Modus, entweder mithilfe des Flags --read-only im Befehl docker run oder mithilfe der Option readOnlyRootFilesystem in Kubernetes. Sie können dies in Kubernetes dadurch erzwingen, dass Sie eine PodSecurityPolicy verwenden.

Möglichst kleines Image erstellen

Wichtigkeit: MITTEL

Das Erstellen von kleineren Images ermöglicht z. B. schnellere Uploads und Downloads, die besonders für die Kaltstartzeit von Pods in Kubernetes von Bedeutung sind: Je kleiner das Image, desto schneller kann es vom Knoten heruntergeladen werden. Eine reduzierte Image-Größe ist aber oft schwierig zu realisieren, da unbeabsichtigt Build-Abhängigkeiten oder nicht optimierte Schichten im endgültigen Image enthalten sein können.

Möglichst kleines Basis-Image verwenden

Das Basis-Image ist das Image, auf das im Dockerfile mit der Anweisung FROM verwiesen wird. Jede andere Anweisung im Dockerfile baut auf diesem Image auf. Je kleiner das Basis-Image ist, desto kleiner ist das endgültige Image und desto schneller kann es heruntergeladen werden. Zum Beispiel ist das Image alpine:3.7 um 71 MB kleiner als das Image centos:7.

Sie können auch das Scratch-Basis-Image verwenden, ein leeres Image, in dem Sie Ihre eigene Laufzeitumgebung erstellen können. Wenn die Anwendung eine statisch verknüpfte Binärdatei ist, kann das Scratch-Basis-Image sehr einfach angewendet werden:

FROM scratch
COPY mybinary /mybinary
CMD [ "/mybinary" ]

Im Projekt distroless finden Sie für eine Reihe verschiedener Sprachen schlanke Basis-Images. In diesen Images sind nur die Laufzeitabhängigkeiten für die Sprache enthalten, nicht jedoch die zahlreichen Tools, die in einer Linux-Distribution üblich sind, wie Shells oder Paketmanager.

Im folgenden Video zu Best Practices von Kubernetes werden zusätzliche Strategien dazu behandelt, wie Sie kleine Container erstellen und gleichzeitig Ihre Anfälligkeit für Sicherheitslücken reduzieren.

Überflüssiges im Image beseitigen

Damit das Image klein bleibt, installieren Sie darin nur, was unbedingt erforderlich ist. Sie könnten versucht sein, zusätzliche Pakete zu installieren und sie dann in einem späteren Schritt zu entfernen. Dieser Ansatz ist jedoch nicht ausreichend. Da mit jeder Anweisung im Dockerfile eine Schicht erstellt wird, nimmt die Größe des Gesamt-Images nicht ab, wenn Sie in einem früheren Schritt erstellte Daten in einem späteren Schritt wieder entfernen. Die Daten sind nach wie vor vorhanden und lediglich in einer tieferen Schicht verborgen. Betrachten Sie dieses Beispiel:

Schlechtes Dockerfile Gutes Dockerfile

FROM debian:9
RUN apt-get update && \ apt-get install -y \ [buildpackage] RUN [build my app] RUN apt-get autoremove --purge \ -y [buildpackage] && \ apt-get -y clean && \ rm -rf /var/lib/apt/lists/*

FROM debian:9
RUN apt-get update && \ apt-get install -y \ [buildpackage] && \ [build my app] && \ apt-get autoremove --purge \ -y [buildpackage] && \ apt-get -y clean && \ rm -rf /var/lib/apt/lists/*

In der schlechten Version des Dockerfiles sind [buildpackage] und die Dateien in /var/lib/apt/lists/* weiterhin in der Schicht vorhanden, die dem ersten RUN entspricht. Diese Schicht ist Teil des Images und muss zusammen mit den übrigen Inhalten hoch- und heruntergeladen werden, auch wenn die darin enthaltenen Daten im endgültigen Image nicht verfügbar sind.

In der guten Version des Dockerfiles werden alle Aufgaben in einer einzigen Schicht ausgeführt, in der nur die erstellte Anwendung enthalten ist. Das [buildpackage] und die Dateien in /var/lib/apt/lists/* sind nicht im endgültigen Image vorhanden, auch nicht verborgen in einer tieferen Schicht.

Weitere Informationen zu Image-Schichten finden Sie unter Docker-Build-Cache optimieren.

Eine weitere gute Methode, um überflüssige Daten im Image zu beseitigen, besteht in der Verwendung von mehrstufigen Builds (eingeführt in Docker 17.05). Bei mehrstufigen Builds können Sie die Anwendung zuerst in einem "Build"-Container erstellen und das Ergebnis in einem anderen Container nutzen. Dabei wird dasselbe Dockerfile verwendet.

Mehrstufiger Build-Prozess von Docker

Abbildung 3. Mehrstufiger Build-Prozess in Docker.

Im folgenden Dockerfile wird die Binärdatei hello in einem ersten Container erstellt und dann in einen zweiten eingefügt. Da der zweite Container auf Scratch basiert, enthält das endgültige Image nur die Binärdatei hello und nicht die Quelldatei und die Objektdateien, die während des Build-Vorgangs benötigt wurden. Die Binärdatei muss statisch verknüpft sein, damit sie funktioniert, ohne dass im Scratch-Image eine externe Bibliothek erforderlich wäre.

FROM golang:1.10 as builder

WORKDIR /tmp/go
COPY hello.go ./
RUN CGO_ENABLED=0 go build -a -ldflags '-s' -o hello

FROM scratch
CMD [ "/hello" ]
COPY --from=builder /tmp/go/hello /hello

Images mit gemeinsamen Schichten erstellen

Wenn Sie ein Docker-Image herunterladen müssen, überprüft Docker zuerst, ob Sie bereits einige der im Image enthaltenen Schichten haben. Wenn dies der Fall ist, werden sie nicht heruntergeladen. Zu dieser Situation kann es kommen, wenn Sie vorher ein anderes Image heruntergeladen haben, das die gleiche Basis wie das Image aufweist, das Sie aktuell herunterladen. Das hat zur Folge, dass für das zweite Image eine viel geringere Datenmenge heruntergeladen wird.

Diese Reduzierung können Sie auf Organisationsebene nutzen, wenn Sie für Ihre Entwickler eine Reihe gebräuchlicher, standardmäßiger Basis-Images bereitstellen. Jedes Basis-Image muss von Ihren Systemen nur einmal heruntergeladen werden. Nach dem ersten Download werden nur die Schichten benötigt, durch die sich ein Image eindeutig unterscheidet. Je mehr Ihre Images also gemeinsam haben, desto schneller können sie heruntergeladen werden.

Images mit gemeinsamen Schichten erstellen

Abbildung 4. Images mit gemeinsamen Schichten erstellen.

Container Registry auf Sicherheitslücken scannen

Wichtigkeit: MITTEL

Sicherheitslücken in Software sind in der Welt der Bare-Metal-Server und virtuellen Maschinen ein bekanntes Problem. Eine gängige Methode zur Bekämpfung dieser Sicherheitslücken liegt im Einsatz eines zentralen Inventarsystems, in dem die auf den einzelnen Servern installierten Pakete aufgelistet sind. Abonnieren Sie für die Upstream-Betriebssysteme die Feeds zu Sicherheitslücken, damit Sie informiert sind, wenn sich eine Sicherheitslücke auf Ihre Server auswirkt, und wenden Sie entsprechende Patches an.

Da Container aber unveränderlich sein müssen (weitere Informationen dazu finden Sie unter Zustandslosigkeit und Unveränderlichkeit von Containern), sollten Sie bei einer Sicherheitslücke keinen Patch darin ausführen. Die Best Practice besteht in diesem Fall darin, das Image mit Patches neu zu erstellen. Container haben einen viel kürzeren Lebenszyklus und eine weniger gut definierte Identität als Server. Die Verwendung eines entsprechenden zentralen Inventarsystems ist daher eine schlechte Methode, um Sicherheitslücken in Containern zu erkennen.

Container Registry hat ein Feature zum Scannen auf Sicherheitslücken, um Ihnen bei diesem Problem zu helfen. Wenn dieses Feature aktiviert ist, werden Sicherheitslücken in Ihren gepackten Container-Images erkannt. Images werden gescannt, wenn sie in Container Registry hochgeladen werden und wenn ein Update der Sicherheitslückendatenbank vorliegt. Sie haben mehrere Möglichkeiten, auf die von diesem Feature gemeldeten Informationen zu reagieren:

  • Erstellen Sie einen cron-ähnlichen Job, mit dem Sicherheitslücken angezeigt werden und der Prozess zur Behebung, falls vorhanden, ausgelöst wird.
  • Sobald eine Sicherheitslücke erkannt wurde, verwenden Sie die Pub/Sub-Integration, um den Patchprozess auszulösen, der in Ihrer Organisation verwendet wird.

Wir empfehlen, den Patchprozess zu automatisieren und die vorhandene Pipeline der kontinuierlichen Integration zu nutzen, die ursprünglich zum Erstellen des Images verwendet wurde. Wenn Sie auf Ihre Pipeline für kontinuierliches Deployment vertrauen, möchten Sie möglicherweise auch, dass das fertige Image automatisch bereitgestellt wird, wenn es fertig ist. Die meisten Nutzer bevorzugen jedoch vor dem Deployment einen manuellen Überprüfungsschritt. Mit dem folgenden Verfahren kann das umgesetzt werden:

  1. Speichern Sie die Images in Container Registry und aktivieren Sie das Scannen auf Sicherheitslücken.
  2. Konfigurieren Sie einen Job, mit dem regelmäßig neue Sicherheitslücken aus Container Registry abgerufen werden und bei Bedarf eine Neuerstellung der Images ausgelöst wird.
  3. Wenn die neuen Images erstellt wurden, lassen Sie sie von Ihrem System für das kontinuierliche Deployment in einer Staging-Umgebung bereitstellen.
  4. Prüfen Sie die Staging-Umgebung manuell auf Probleme.
  5. Wenn Sie keine finden, lösen Sie manuell das Deployment in der Produktionsumgebung aus.

Images ordnungsgemäß taggen

Wichtigkeit: MITTEL

Docker-Images werden im Allgemeinen mit zwei Angaben identifiziert: ihrem Namen und ihrem Tag. Für das Image google/cloud-sdk:193.0.0 ist beispielsweise google/cloud-sdk der Name und 193.0.0 das Tag. Das Tag latest wird standardmäßig verwendet, wenn Sie keines in Ihren Docker-Befehlen angeben. Das Name-/Tag-Paar ist zu jeder Zeit eindeutig. Sie können ein Tag jedoch ganz nach Bedarf einem anderen Image zuweisen.

Wenn Sie ein Image erstellen, müssen Sie es ordnungsgemäß taggen. Gehen Sie nach einer kohärenten und konsistenten Tagging-Richtlinie vor. Dokumentieren Sie die Tagging-Richtlinie so, dass sie für Nutzer von Images leicht verständlich ist.

Container-Images bieten die Möglichkeit, eine Software zu verpacken und freizugeben. Durch das Taggen des Images können Nutzer eine bestimmte Version Ihrer Software vor dem Herunterladen identifizieren. Stimmen Sie daher das Tagging-System für Container-Images mit der Freigaberichtlinie für die Software ab.

Mit semantischer Versionierung taggen

Eine gängige Methode bei der Veröffentlichung von Software ist die Kennzeichnung einer bestimmten Version des Quellcodes mit einer Versionsnummer, z. B. mit dem Befehl git tag. Die Spezifikation der semantischen Versionsverwaltung bietet eine praktikable Methode für die Verwaltung von Versionsnummern. In diesem System weist Ihre Software eine dreiteilige Versionsnummer auf: X.Y.Z. Dabei gilt:

  • X steht für die Nummer der Hauptversion, die nur bei nicht kompatiblen API-Änderungen erhöht wird.
  • Y steht für die Nummer der Nebenversion, die bei neuen Features erhöht wird.
  • Z steht für die Nummer der Patchversion, die bei Fehlerkorrekturen erhöht wird.

Jede Erhöhung der Nummer der Neben- oder Patchversion muss sich auf eine abwärtskompatible Änderung beziehen.

Wenn Sie dieses oder ein ähnliches System verwenden, taggen Sie Ihre Images gemäß der folgenden Richtlinie:

  • Das Tag latest bezieht sich immer auf das neueste (möglichst stabile) Image. Dieses Tag wird verschoben, sobald ein neues Image erstellt wird.
  • Das Tag X.Y.Z bezieht sich auf eine bestimmte Version der Software. Verschieben Sie es nicht auf ein anderes Image.
  • Das Tag X.Y bezieht sich auf den neuesten Patchrelease des Nebenzweigs X.Y der Software. Es wird verschoben, wenn eine neue Patchversion veröffentlicht wird.
  • Das Tag X bezieht sich auf den neuesten Patchrelease des neuesten Nebenrelease des Hauptzweigs X. Es wird verschoben, wenn entweder eine neue Patchversion oder eine neue Nebenversion veröffentlicht wird.

Mit dieser Richtlinie können Nutzer flexibel auswählen, welche Version der Software sie verwenden möchten. Wenn sie eine bestimmte Version (X.Y.Z) ausgewählt haben, können sie sich sicher sein, dass sich das Image niemals ändert, oder sie können automatisch Updates abrufen lassen, wenn sie ein weniger spezifisches Tag auswählen.

Mit dem Commit-Hashwert von Git taggen

Wenn Sie ein fortschrittliches System für Continuous Delivery einsetzen und häufig Releases Ihrer Software veröffentlichen, verwenden Sie vermutlich keine Versionsnummern wie in der Spezifikation der semantischen Versionierung beschrieben. In diesem Fall besteht eine übliche Methode zum Umgang mit Versionsnummern in der Verwendung des Commit-SHA-1-Hashwerts von Git (oder einer Kurzversion davon) als Versionsnummer. Der Commit-Hashwert von Git ist standardmäßig unveränderlich und verweist auf eine bestimmte Version Ihrer Software.

Diesen Commit-Hashwert können Sie als Versionsnummer für die Software verwenden, aber auch als Tag für das Docker-Image, das aus dieser spezifischen Version der Software erstellt wurde. Auf diese Weise können Docker-Images nachverfolgt werden: Da in diesem Fall das Image-Tag unveränderlich ist, wissen Sie sofort, welche Version Ihrer Software jeweils in einem bestimmten Container ausgeführt wird. Automatisieren Sie in Ihrer Pipeline für kontinuierliche Bereitstellung die Aktualisierung der für die Deployments verwendeten Versionsnummern.

Verwendung eines öffentlichen Images sorgfältig abwägen

Wichtigkeit: –

Einer der großen Vorteile von Docker liegt in der großen Anzahl öffentlich verfügbarer Images für alle Arten von Software. Diese Images ermöglichen einen schnellen Einstieg. Wenn Sie jedoch eine Containerstrategie für Ihre Organisation entwerfen, wird es möglicherweise Einschränkungen geben, für die öffentlich bereitgestellte Images nicht geeignet sind. Die folgenden Einschränkungen machen z. B. die Verwendung öffentlicher Images unmöglich:

  • Sie möchten genau kontrollieren, was in Ihren Images enthalten ist.
  • Sie möchten nicht auf ein externes Repository angewiesen sein.
  • Sie möchten Sicherheitslücken in Ihrer Produktionsumgebung streng kontrollieren.
  • Sie wünschen in jedem Image dasselbe Basisbetriebssystem.

Auf alle diese Einschränkungen lautet die Antwort gleich und ist kostenaufwendig: Sie müssen Ihre eigenen Images erstellen. Das Erstellen eigener Images ist bei einer begrenzten Anzahl von Images nicht allzu problematisch, aber in der Regel nimmt diese Anzahl rasch zu. Damit Sie die Verwaltung eines solch umfangreichen Systems bewältigen können, sollten Sie den Einsatz folgender Methoden und Hilfsmittel prüfen:

  • Automatisierte Methode zum zuverlässigen Erstellen von Images, auch von selten erstellten Images: Dafür eignen sich Build-Trigger in Cloud Build.
  • Standardisiertes Basis-Image: Google stellt einige Basis-Images zur Verfügung, die Sie verwenden können.
  • Automatisierte Methode zum Verteilen von Updates des Basis-Images an untergeordnete Images.
  • Methode zur Behebung von Sicherheitslücken in Ihren Images: Weitere Informationen finden Sie unter Scannen nach Sicherheitslücken in Container Registry nutzen.
  • Methode zur verbindlichen Festlegung interner Standards für die von den verschiedenen Teams in Ihrer Organisation erstellten Images.

Zur verbindlichen Festlegung von Richtlinien für die von Ihnen erstellten Images stehen verschiedene Tools zur Verfügung:

  • container-diff analysiert den Inhalt von Images und vergleicht auch die Inhalte zweier Images.
  • container-structure-test prüft, ob die Inhalte eines Images den von Ihnen festgelegten Regeln entsprechen.
  • Grafeas ist eine Artefakt-Metadaten-API, mit der Sie Metadaten zu Ihren Images speichern, um später prüfen zu können, ob diese Images Ihren Richtlinien entsprechen.
  • Kubernetes verfügt über Admission-Controller, mit denen Sie eine Reihe von Voraussetzungen prüfen können, bevor Sie eine Arbeitslast in Kubernetes erstellen.
  • Ferner verfügt Kubernetes über Pod-Sicherheitsrichtlinien, mit denen Sie die Verwendung von Sicherheitsoptionen im Cluster erzwingen können.

Eventuell möchten Sie auch ein Hybridsystem einsetzen, bei dem Sie ein öffentliches Image wie Debian oder Alpine als Basis-Image nutzen und alles, was sonst noch benötigt wird, auf der Grundlage dieses Images erstellen. Es kann aber auch sein, dass Sie für einige nicht kritische Images öffentliche Images nutzen und in anderen Fällen eigene Images erstellen möchten. Es gibt hier keine Patentlösungen, aber auf jeden Fall müssen Sie sich mit der genauen Vorgehensweise auseinandersetzen.

Hinweis zu Lizenzen

Bevor Sie Bibliotheken und Pakete von Drittanbietern in Ihr Docker-Image einbinden, prüfen Sie, ob dies mit den jeweiligen Lizenzen vereinbar ist. In den Lizenzen von Drittanbietern können auch Einschränkungen hinsichtlich der Weiterverteilung vorgesehen sein, die gelten, wenn Sie ein Docker-Image in einer öffentlichen Registry veröffentlichen.

Nächste Schritte

Weitere Google Cloud-Features mit unseren Anleitungen testen