Best Practices für Go-Anwendungen in Docker-Containern
Jekayin-Oluwa Olabemiwo
23. März 2022
0 Min. LesezeitGo-Anwendungen und Container passen perfekt zusammen. Die kleinen Binärdateien von Go-Anwendungen eignen sich ideal für die Microservices-Bereitstellungsstrategien, die Docker und Kubernetes besonders gut unterstützen. Diese Synergie bringt jedoch auch Herausforderungen mit sich. Daher ist es wichtig, Best Practices für Container und zentrale Konzepte zu verstehen, um Sicherheitsrisiken zu vermeiden, die sich leicht in Ihre Container-Images einschleichen können.
In diesem Artikel programmieren Sie eine beispielhafte Go-Anwendung und erfahren, wie Sie sie am besten containerisieren und sicher ausführen.
Voraussetzungen
Für dieses Tutorial müssen Docker und Go auf Ihrem Rechner installiert sein. Außerdem sollten Sie mit beiden grundlegende Erfahrung haben. Eine Einführung und Downloads für Docker finden Sie auf der Seite Orientierung und Einrichtung in der Docker-Dokumentation. Gute Go-Tutorials finden Sie in der offiziellen Go-Dokumentation oder direkt auf der Seite zur Go-Installation für Linux.
Eine beispielhafte Go-Anwendung erstellen
Erstellen wir zunächst unsere Go-API. Wechseln Sie dazu in das Verzeichnis, in dem Ihre Go-Anwendung auf Ihrem Rechner gespeichert werden soll. Erstellen Sie anschließend ein Verzeichnis mit dem Namen „godocker“. Führen Sie in diesem Verzeichnis godocker/ den folgenden Befehl aus, um Ihr Go-Modul zu definieren:
Erstellen Sie als Nächstes im Verzeichnis „godocker“ eine Datei namens „main.go“. Diese Datei enthält Ihren API-Code. Nun können Sie Logik hinzufügen, um über die API die aktuelle Uhrzeit bereitzustellen und zu verarbeiten.
Geben Sie den folgenden Code in godocker/main.go ein:
Dieser Code stellt die aktuelle Uhrzeit über die API bereit und verwendet dazu die Variable TimeFormat des http-Pakets. Anschließend definieren wir die Bibliothek NewServeMux als mux, um den HTTP-Server zu konfigurieren. Schließlich registrieren wir den Endpunkt /time, damit die API die aktuelle Uhrzeit zurückgibt und die Antwort am Endpunkt /time als JSON codiert.
Führen wir einen kurzen Test durch. Starten Sie die Anwendung mit dem folgenden Befehl in der Shell:
Die Ausgabe sollte ungefähr so aussehen:
Testen wir nun die API in einem anderen Terminalfenster mit cURL. Geben Sie im Terminal den folgenden Befehl ein:
Die Ausgabe sollte so aussehen:
Die Dockerfile vorbereiten
Eine Dockerfile enthält eine Reihe von Anweisungen, mit denen eine Anwendung als Container gepackt und bereitgestellt wird. In diesem Abschnitt erstellen wir eine Dockerfile und sehen uns einige Anweisungen an, mit denen wir unsere beispielhafte Anwendung als Container packen können.
Die Docker-Syntaxversion angeben
Zunächst fügen wir die Syntax-Direktive hinzu. Erstellen Sie im Verzeichnis godocker/ eine neue Datei namens „Dockerfile“ und geben Sie in der ersten Zeile den folgenden Code ein:
Die Syntax-Direktive gibt den Speicherort der Dockerfile-Syntax an, die wir zum Erstellen unserer Dockerfile verwenden. Diese Codezeile legt den Speicherort der Dockerfile-Syntax als docker/dockerfile:1 fest – die neueste veröffentlichte Syntaxversion. Docker prüft die Syntaxversion, bevor es das Buildkit-Backend zum Erstellen der Dockerfile verwendet.
Achten Sie darauf, dass diese Zeile am Anfang der Datei auskommentiert ist. Nach der Syntax-Direktive folgt üblicherweise eine Leerzeile.
Ein Basis-Image mit geringem Speicherbedarf übernehmen
Als Nächstes geben wir mit der FROM-Anweisung das Docker-Basis-Image an, das wir übernehmen möchten.
Fügen Sie den folgenden Code zu godocker/Dockerfil` hinzu:
Mit dieser Anweisung müssen wir kein eigenes Docker-Basis-Image erstellen. Stattdessen übernehmen wir das offizielle Docker-Image für Go-Anwendungen in der Alpine-Linux-Variante. Die Go-Version des Basis-Images ist 1.17. Das alpine-Image ist im Vergleich zu einer Variante wie dem ubuntu-Image sehr klein.
Nun ergänzen wir den Rest der Dockerfile und fügen Kommentare in den Code ein, um den Zweck der einzelnen Zeilen kurz zu erläutern.
Aktualisieren Sie godocker/Dockerfile, sodass die Datei den folgenden Code enthält:
Das Image erstellen
Nachdem unsere Dockerfile fertig ist, erstellen wir mit dem Befehl docker build daraus unser Docker-Image. Docker verwendet den Docker-Daemon, um Images zu erstellen. Mit der Option --tag – abgekürzt -t – legen wir mit dem Befehl docker build einen benutzerdefinierten Namen für unser Docker-Image fest.
Geben Sie den folgenden Befehl im Terminal ein:
Wir hängen . an den Befehl docker build an, damit das Image im aktuellen Verzeichnis erstellt wird, das als Build-Kontext dient. Verwenden Sie für den Build-Kontext nicht den Pfad /, da dabei der gesamte Quellcode an den Docker-Daemon übertragen werden kann.
Ihre Build-Ausgabe sollte die Zeile FINISHED enthalten und ungefähr so aussehen:
Diese Ausgabe bedeutet, dass das Docker-Image vollständig erstellt und mit dem Tag godocker versehen wurde.
Geben Sie den folgenden Befehl im Terminal ein, um die Liste der lokalen Images anzuzeigen:
Ihre Ausgabe sollte ungefähr so aussehen:
Das neu erstellte godocker-Image in unserer Ausgabe ist 319 MB groß. Für eine einfache API-Anwendung ist das viel. Daher müssen wir den Build optimieren und ein schlankeres Image erstellen. Im nächsten Abschnitt verwenden wir Multi-Stage-Builds, um einen schlanken Build zu erzielen.
Multi-Stage-Builds verwenden
Mit Multi-Stage-Builds lassen sich deutlich kleinere Images erstellen als mit dem Single-Stage-Ansatz aus dem vorherigen Abschnitt. Bei einem Multi-Stage-Build wird ein Image verwendet, um einzelne Bestandteile zu erstellen. Diese werden in ein kleineres Image gepackt, das nur die für ihre Ausführung notwendigen Komponenten enthält. Indem wir unsere Images auf das für die Anwendung erforderliche Minimum reduzieren, können wir das Risiko von Sicherheitslücken verringern. Dazu verwenden wir mehrere FROM-Anweisungen in unserer Dockerfile.
Das offizielle Scratch-Image verwenden
Wir können unseren Build mit einem leeren Image beginnen, indem wir das scratch-Image des offiziellen Docker-Images übernehmen. In diesem Abschnitt zeigen wir, wie Sie das Image scratch für Multi-Stage-Builds verwenden.
Wechseln Sie zunächst in das Stammverzeichnis Ihrer Anwendung. Erstellen Sie dann eine Datei namens „Dockerfile.multistage“ und geben Sie den folgenden Code ein:
Dieser Code gibt das Basis-Image an, das vom offiziellen Image golang:1.17-alpine übernommen wird, und benennt die Stage build. Anschließend verwenden wir eine weitere FROM-Anweisung, um das Multi-Stage-Konzept umzusetzen: Dabei kopieren wir die erstellte Binärdatei aus der ersten Stage in das leere Image der zweiten Stage.
Als Nächstes müssen wir mit der neuen Datei Dockerfile.multistage ein neues Image erstellen. Außerdem müssen wir dem neuen Image den Tag „multistage“ geben. So können wir es von dem zuvor erstellten Image unterscheiden.
Geben Sie den folgenden Befehl im Terminal ein:
Prüfen Sie nach dem erfolgreichen Build die Image-Liste, indem Sie den folgenden Befehl im Terminal eingeben:
Ihre Ausgabe sollte ungefähr so aussehen:
Diese Ausgabe zeigt den deutlichen Größenunterschied zwischen den Images godocker:multistage und godocker:latest. Das Single-Stage-Image wurde von 319 MB auf 6,1 MB beim Multi-Stage-Image verkleinert – eine klare Verbesserung. Da Container schnell gestartet werden müssen, sind solche Optimierungen beim Containerisieren von Go-Anwendungen entscheidend.
Den Container bereitstellen
Neben der Optimierung von Leistung und Effizienz müssen wir auch überlegen, wie wir unsere Container am besten bereitstellen, damit sie sicher ausgeführt werden. In dieser Phase des Tutorials setzen wir einige Best Practices um.
Als Nicht-Root-Benutzer ausführen
Das Prinzip der geringsten Berechtigungen erfordert, dass wir den Zugriff auf Systemressourcen begrenzen. Unsere Go-Docker-Container sind Anwendungscontainer und müssen nicht mit Root-Rechten ausgeführt werden. Daher sollten wir zur Erhöhung der Sicherheit in unserer Dockerfile einen neuen Benutzer und eine Gruppe mit eingeschränktem Zugriff erstellen.
Um einen Nicht-Root-Benutzer zu erstellen, fügen Sie die folgenden Zeilen direkt nach der ersten FROM-Anweisung in godocker/Dockerfile.multistage ein:
Mit dieser Anweisung werden die Argumente USERNAME und PASSWORD mit dem Schlüsselwort ARG festgelegt. Anschließend wird mit der Anweisung RUN adduser ein Benutzer erstellt.
Als Nächstes fügen wir Anweisungen hinzu, um die Benutzerangaben aus der ersten Stage zu kopieren und in der zweiten Stage anzuwenden.
Fügen Sie den fett gedruckten Code im folgenden Beispiel zur zweiten Stage in godocker/Dockerfile.multistage hinzu. Die Datei sollte dann mit dem folgenden Code enden:
In Kubernetes können Sie runAsuser: UID im Feld securityContext angeben. In der Kubernetes-Dokumentation erfahren Sie, wie Sie den Sicherheitskontext für einen Pod festlegen.
Ein schreibgeschütztes Root-Dateisystem verwenden
Eine weitere Möglichkeit, die Sicherheit unserer Anwendung zu erhöhen, besteht darin, den Container mit einem schreibgeschützten Dateisystem auszuführen. Dazu übergeben wir dem Befehl docker run das Flag für den schreibgeschützten Zugriff.
Geben Sie den folgenden Befehl im Terminal ein:
Linux-Capabilities entfernen oder verweigern
Linux-Capabilities sind Berechtigungen, die wir unter Linux aktivieren oder deaktivieren können. Da unser Container auf einer Linux-Variante basiert, können wir diese Capabilities ebenfalls nutzen, um die Sicherheit unserer Anwendung zu erhöhen. Das Entfernen bestimmter Capabilities verringert das Risiko für unseren Container.
Entfernen wir in diesem Tutorial alle Capabilities außer setuid.
Geben Sie den folgenden Befehl im Terminal ein:
CPU- und Speichernutzung begrenzen
Mit dem Befehl docker run können wir die Ressourcennutzung unseres Docker-Containers auf dem Host-Rechner begrenzen. Dazu verwendet Docker das Flag --cpus.
Um beispielsweise zu verhindern, dass der Container mehr als 50 % einer einzelnen CPU nutzt, geben Sie den folgenden Befehl in der Docker-CLI ein:
Wenn Sie zwei CPUs verwenden möchten, können Sie die Nutzung mit dem folgenden Befehl in der Docker-CLI begrenzen:
Wenn Sie die Speichernutzung eines Docker-Containers auf 1.024 MB begrenzen möchten, können Sie den Befehl docker run wie folgt verwenden:
Mit diesem Befehl wird außerdem ein Speicherlimit von 256 MB festgelegt, das greift, wenn Docker erkennt, dass auf dem Host nur noch wenig Speicher verfügbar ist.
Fazit
In diesem Artikel haben wir eine Go-Anwendung eingerichtet und mit Docker containerisiert. Außerdem haben wir Multi-Stage-Builds zur Leistungsoptimierung eingesetzt, mit einem leeren offiziellen Docker-Image ein Image von Grund auf erstellt und einige Best Practices besprochen, die beim Erstellen und Bereitstellen von Containern hilfreich sein können. Diese Richtlinien bieten einen Einstieg in die effiziente und sichere Containerisierung von Go-Webanwendungen mit Docker sowie in ein effektives Speichermanagement.
Weitere Best Practices für Sicherheit finden Sie im Ressourcen-Center Snyk Learn.
