Erstellen eines minimalen Docker-Containers für Go-Apps

Hallo Habr! Ich mache Sie auf eine Übersetzung des Artikels des Gründers des Meetspaceapp Nick Gauthier-Dienstes „Building Minimal Docker Containers for Go Applications“ aufmerksam .

10 Minuten zum Lesen

Es gibt viele offizielle und von der Community unterstützte Container für verschiedene Programmiersprachen (einschließlich Go). Diese Behälter können jedoch sehr groß sein. Vergleichen wir zunächst die Standardmethoden zum Erstellen von Containern für Go-Apps und zeigen Ihnen dann, wie Sie extrem kleine statische Container-Go-Apps erstellen

Teil 1: Unsere "Bewerbung"


Zum Testen benötigen wir eine kleine Anwendung. Lassen Sie uns google.com teilen und die HTML-Größe ausgeben.

package main import ( "fmt" "io/ioutil" "net/http" "os" ) func main() { resp, err := http.Get("https://google.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body)) } func check(err error) { if err != nil { fmt.Println(err) os.Exit(1) } } 

Wenn wir anfangen, bekommen wir nur eine Nummer. Ich habe ungefähr 17K. Ich habe mich bewusst für SSL entschieden, werde den Grund aber später erläutern.

Teil 2: Dockerisierung


Mit dem offiziellen Go-Image schreiben wir "onbuild" Dockerfile:

 FROM golang:onbuild 

Das "Onbuild" -Image setzt voraus, dass Ihr Projekt eine Standardstruktur hat und eine Standard-Go-Anwendung erstellt. Wenn Sie mehr Flexibilität benötigen, können Sie das Standard-Go-Image verwenden und selbst kompilieren:

 FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"] 

Es wäre schön, hier ein Makefile oder ähnliches zu erstellen, das Sie für die Anwendungserstellung verwenden. Wir könnten einige Ressourcen aus dem CDN laden oder sie aus einem anderen Projekt importieren, oder wir möchten vielleicht Tests im Container ausführen ...
Wie Sie sehen, ist die Dockerisierung von Go ziemlich einfach, insbesondere wenn Sie bedenken, dass wir nicht die Dienste und Ports verwenden, zu denen wir eine Verbindung herstellen müssen. Aber es gibt einen schwerwiegenden Fehler in offiziellen Bildern - sie sind wirklich groß. Mal sehen:

 REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB 

Das Basis-Image nimmt 514,9 MB ein, und unsere Anwendung fügt weitere 5,8 MB hinzu. Wie kommt es, dass unsere kompilierte Anwendung 515 MB Abhängigkeiten benötigt?
Tatsache ist, dass unsere Anwendung im Container kompiliert wurde. Dies bedeutet, dass der Container Go installieren muss. Daher benötigt er Go-Abhängigkeiten sowie einen Paketmanager und ein wirklich ganzes Betriebssystem. Wenn Sie sich die Docker-Datei für golang: 1.4 ansehen, wird sie mit Debian Jessie geliefert, installiert den GCC-Compiler und erstellt Tools, lädt Go herunter und installiert sie. Somit erhalten wir den gesamten Debian-Server und das Go-Toolkit zum Starten unserer winzigen Anwendung. Was kann man dagegen tun?

Teil 3: Kompilieren!


Sie können die Situation verbessern, indem Sie leicht vom üblichen Ansatz abweichen. Dazu kompilieren wir Go in unser Arbeitsverzeichnis und fügen dann die Binärdatei zum Container hinzu. Dies bedeutet, dass ein einfacher Docker-Build nicht funktioniert. Wir brauchen eine mehrstufige Containermontage:

 go build -o main . docker build -t example-scratch -f Dockerfile.scratch . 

Und eine einfache Dockerfile.scratch:

 FROM scratch ADD main / CMD ["/main"] 

Was ist Kratzer? Scratch ist ein spezielles leeres Bild im Docker. Seine Größe ist 0B:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 5.60MB scratch latest 511136ea3c5a 22 months ago 0B 

Dadurch nimmt unser Container nur 5,6 MB ein. Großartig! Es gibt jedoch ein Problem:

 $ docker run -it example-scratch no such file or directory 

Was bedeutet das? Es dauerte eine Weile, bis mir klar wurde, dass unsere Go-Binärdatei nach Bibliotheken auf dem Betriebssystem suchte, auf dem sie ausgeführt wurde. Wir haben unsere Anwendung kompiliert, sie ist jedoch weiterhin dynamisch mit den Bibliotheken verknüpft, die gestartet werden müssen (d. H. Mit allen C-Bibliotheken). Leider ist der Arbeitsbereich leer, sodass keine Bibliotheken oder Ladepfade vorhanden sind. Wir müssen das Build-Skript ändern, um unsere Anwendung statisch mit allen integrierten Bibliotheken zu kompilieren:

 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 

Wir deaktivieren cgo, wodurch wir eine statische Binärdatei erhalten. Wir geben auch Linux als Betriebssystem an (falls jemand es auf Mac oder Windows erstellt). Das Flag -a bedeutet, dass alle von uns verwendeten Pakete neu erstellt werden, wodurch alle Importe mit deaktiviertem cgo neu erstellt werden. Jetzt haben wir eine statische Binärdatei. Lass uns laufen:

 $ docker run -it example-scratch Get https://google.com: x509: failed to load system roots and no roots provided 

Was ist das Aus diesem Grund habe ich mich in unserem Beispiel für SSL entschieden. Dies ist eine sehr häufige „Überhöhung“ für solche Szenarien: Um SSL-Anforderungen abzuschließen, benötigen wir Root-SSL-Zertifikate. Wie fügen wir sie unserem Container hinzu?
Je nach Betriebssystem befinden sich Zertifikate möglicherweise an verschiedenen Orten. Für viele Linux-Distributionen ist dies /etc/ssl/certs/ca-certificates.crt . Zunächst kopieren wir ca-certificates.crt von unserem Computer (oder einer virtuellen Linux-Maschine oder einem Online-Zertifikatanbieter) in unser Repository. Dann fügen wir ADD zu unserer Docker-Datei hinzu, um diese Datei dorthin zu verschieben, wo Go sie erwartet:

 FROM scratch ADD ca-certificates.crt /etc/ssl/certs/ ADD main / CMD ["/main"] 

Erstellen Sie jetzt einfach unser Image neu und starten Sie es. Es funktioniert! Sehen wir uns jetzt die Größe unserer Anwendung an:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 6.12MB example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB scratch latest 511136ea3c5a 22 months ago 0B 

Wir haben etwas mehr als ein halbes Megabyte hinzugefügt (und das meiste davon stammt aus einer statischen Datei und nicht aus Stammzertifikaten). Wir haben einen wirklich kleinen Container - es wird sehr praktisch sein, ihn zwischen den Registern zu verschieben.

Fazit


Unser Ziel war es, die Containergröße für die Go-Anwendung zu reduzieren. Die Besonderheit von Go ist, dass es eine statisch verknüpfte Binärdatei erstellen kann, die die Anwendung vollständig enthält. Das können auch andere Sprachen, aber keineswegs alle. Die Anwendung einer ähnlichen Technik zur Reduzierung der Containergröße in anderen Sprachen hängt von deren Mindestanforderungen ab. Beispielsweise kann eine Java- oder JVM-Anwendung außerhalb des Containers kompiliert und dann in einen Container eingebettet werden, der nur die JVM (und ihre Abhängigkeiten) enthält. Trotzdem ist es weniger als ein Container mit JDK.

Source: https://habr.com/ru/post/de460535/


All Articles