Für das Deployment von Microservices haben sich Docker-Container, die jeweils einen Service beinhalten, etabliert. So lassen sich die Services sauber trennen und auch einzeln verwalten.
Container in so solch einem Umfeld sollten nur noch die minimal benötigten Abhängigkeiten beinhalten. Das spart Plattenplatz, Übertragungsgschwindigkeit und ist auch aus Sicherheitsgründen angebracht. Was nicht installiert ist, kann auch nicht als potentielles Eintrittstor verwendet werden.
In diesem Artikel geht es um den Bau eines minimalen Docker Image für die eigene Anwendung. Bei der Beispiel-Anwendung handelt es sich in diesem Fall um eine Go “Hello World” Application, die nur den Bau eines Images zeigen soll.
package main
import "fmt"
func main() {
fmt.Printf("Hello World\n")
}
Wer für Go Anwendungen Docker Images baut stolpert früher oder später über das offizielle Golang Docker Image. Dies beinhaltet den Go Compiler und kann in vielen Versionen bezogen werden. Die neuste Version erhält man immer mittels:
docker pull golang:latest
Die erste Idee ist natürlich genau dieses Image als Basis für die Anwendung zu benutzen.
FROM golang:latest
COPY *.go .
RUN go build -o app *.go
CMD ["/go/app"]
Das Image läßt sich über folgenden Befehl bauen und als Container ausführen:
$ docker build -t go-first .
$ docker run --rm go-first
Hello World
Das war einfach! Allerdings sieht man, wenn man sich die Größe des Images anschaut:
kkoehler@precision:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-first latest 9b8c70ab2d00 <date> 774MB
774 MB! Das kann man eher nicht als minimales Images bezeichnen…
Go Anwendungen werden statisch gelinkt. Das heißt, dass der Compiler alle Abhängigkeiten in das resultierende Binary packt. Neben dem Binary wird nichts mehr benötigt. Durch dieses Feature ist das Übertragen der gebauten Anwendung auf eine andere Umgebung recht einfach. Das Kopieren dieses Binaries reicht, neben eventueller Konfigurationsdateien, völlig aus.
Die Idee ist also recht einfach: Die Anwendung wird mit einem offiziellen Go-Docker Image gebaut allerdings nicht in dieser ausgeführt. Das soll in einem zweiten, “größenoptimierten”, Container erfolgen. Docker kennt für solche Szenarien ein Feature namens Multistage Build.
Multistage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.
Bis zur Einführung dieses Features mussten 2 Dockerfiles gepflegt werden. Eines für den Bau der Anwendung, das andere für die Produktionsumgebung. Der Vorgang wurde auch als “Builder Pattern” bezeichnet. Für den Bau der Images wurde dann meist zusätzlich noch ein Shell-Skript benötigt, mit dem man den Ablauf gesteuert hat.
Beim Einsatz eines Multistage-Builds können Dockerfiles mehrere FROM
Statements enthalten, die für eine einzelne Stage stehen. Jede Stage kann sich in ihrem FROM
Statement auf ein anderes Base-Image basieren. Zwischen den Stages können Daten kopiert werden und somit nur die wirklich benötigten Artefakte von einem Image zum nächsten kopiert werden.
Im Beispiel würde das bedeuten, dass im ersten Container gebaut wird und nur das gebaute Artefakt in das nächste Image kopiert wird. Dieses kann dann “größenoptimiert” sein.
Jede Stage kann einen Namen zugeordnet bekommen, auf den man sich später beziehen kann.
FROM golang as builder
Bei einem späteren Kopieren von Dateien kann angegeben werden von welcher Stage die Dateien kommen sollen.
COPY --from=builder /go/bin/docker-hello .
Für unsere einfache Go Anwendung sieht das fertige Dockerfile dann so aus:
FROM golang as builder
WORKDIR $GOPATH/src/docker-hello
COPY *.go .
RUN CGO_ENABLED=0 go install
# Build the image to run
FROM alpine:latest
COPY --from=builder /go/bin/docker-hello .
CMD ["./docker-hello"]
Beim Einsatz von Alpine-Linux muss die Golang Anwendung mit dem CGO_ENABLED flag übersetzt werden, da Alpine-Linux nicht mit den benötigten glibcs ausgeliefert wird sondern mit musl.
Gebaut werden kann das Image dann mit einem einzigen Befehl:
$ docker build -t go-first-small .
Lässt man sich die Größe des Images anzeigen sieht das schon eher nach einem kleinen Docker Image aus: 7.5MB
kkoehler@precision:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-first-small latest 8895c16f2166 <date> 7.54MB
Lauffähig ist das ganze noch immer ;-)
$ docker build -t go-first-small .
$ docker run --rm go-first-small
Hello World
Viel Erfolg beim Dockern!
16.05.2019
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com