Docker – optymalizacja obrazu z wykorzystaniem „Multi-Stage Builds”

   Automatyzacja Docker 0 Comments

Co jest ważne przy budowanie kontenerów w Dockerze? Oczywiście rozmiar ich obrazów. Zawsze chcemy, aby obraz na którym zbudujemy kontener zajmował jak najmniej miejsca i był przy tym funkcjonalny. Wyobraźmy sobie jednak sytuację w której chcemy uruchomić w kontenerze naszą aplikację napisaną np. w Go. Do tego celu oczywiście wykorzystamy oficjalny obraz golang:latest. Powiedzmy, że nasz Dockerfile wygląda tak:

# obraz z którego wychodzimy
FROM golang:latest

# budujący 
MAINTAINER Daniel Wyrzyński

# ustawiamy katalog roboczy i kopiujemy naszą aplikacje
WORKDIR /go/zrodla/moje_aplikacje
COPY aplikacja.go .

# budujemy aplikacje
RUN go build -o aplikacja .

# uruchamiany naszą aplikacje
CMD ["./aplikacja"]

Natomiast nasz plik aplikacja.go, to prosty „Hello World!”:

package main

import "fmt"

func main() {
        fmt.Println("Witaj czytelniku rm-rf.blog")
}

Zbudujmy kontener:

[rm-rf@localhost golang]# docker build -t aplikacja:latest .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM golang:latest
 ---> 138bd936fa29
Step 2/6 : MAINTAINER Daniel Wyrzyński
 ---> Running in 35452f65a6f4
Removing intermediate container 35452f65a6f4
 ---> a6aca6f3bf66
Step 3/6 : WORKDIR /go/zrodla/moje_aplikacje
Removing intermediate container 498f7de6e43e
 ---> 620674789c3d
Step 4/6 : COPY aplikacja.go .
 ---> c763638f7306
Step 5/6 : RUN go build -o aplikacja .
 ---> Running in 66849812efac
Removing intermediate container 66849812efac
 ---> 57218db73b79
Step 6/6 : CMD ["./aplikacja"]
 ---> Running in b65656d7ae53
Removing intermediate container b65656d7ae53
 ---> 98ee9166994d
Successfully built 98ee9166994d
Successfully tagged aplikacja:latest

I w sumie osiągnęliśmy to co chcieliśmy, mamy kontener w którym uruchomiona będzie nasza aplikacja,więc w czym problem? Tutaj z odpowiedzią przychodzi nam ta informacja:

[rm-rf@localhost golang]# docker images
REPOSITORY      TAG       IMAGE ID        CREATED                SIZE
aplikacja       latest    98ee9166994d    19 seconds ago         735MB

Nasz obraz do malutkiej aplikacji, którą chcieliśmy testować zajmuję całe 735MB. Skąd taki rozmiar? stąd, że obraz golang jest przygotowany w taki sposób, aby znajdowały się w nim wszystkie komponenty potrzebne do budowania naszych projektów. A więc pełny toolchain, który potrzebujemy tak naprawdę tylko do zbudowania naszego kodu, a to wszystko swoje „waży” i niekoniecznie jest nam dalej potrzebne. Chcielibyśmy, żeby sama aplikacja została zbudowana w golang a następnie uruchomiona w innym małym kontenerze jak alpine. Do niedawna radzono sobie z tym w różny sposób, budowało się kontener np. z golang z wystawionym wolumenem w którym była nasza aplikacją, następnie można było ją przerzucić do następnego kontenera, który chcieliśmy budować. W efekcie, potrzebowaliśmy dwa oddzielne pliki Dockerfil, a jeśli chcieliśmy to sobie zautomatyzować to dodatkowo skrypt w bashu.

I tutaj z pomocą przychodzi nam Multi Stage Builds który jest dostępny od wersji 17.05+. Dzięki temu cały proces zmieścimy tylko w jednym Dockerfile. Spójrzmy jak taki plik wygląda tym razem:

# obraz do zbudowania aplikacji
FROM golang:latest

# budujący
MAINTAINER Daniel Wyrzyński

# ustawiamy katalog roboczy i kopiujemy naszą aplikacje
WORKDIR /go/zrodla/moje_aplikacje
COPY aplikacja.go .

# budujemy aplikacje
RUN go build -o aplikacja .

# obraz w którym będziemy testować naszą aplikację
FROM alpine:latest

RUN apk --no-cache add ca-certificates

# katalog dla aplikacji
WORKDIR /appki/

# Kopiujemy naszą zbudowaną aplikację z kontenera golang
COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja .

# uruchamiamy aplikacje w kontenerze zbudowanym na Alpine
CMD ["./aplikacja"]

Jak widzimy, możemy bez problemu użyć 2x FROM, co wcześniej było niemożliwe. Do tego mamy możliwość skopiowania pliku bezpośrednio z pierwszego kontenera który budowaliśmy czyli:

COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja .

Pojawiło się --from=0 co oznacza, że plik będzie kopiowany z kontenera 0, czyli pierwszego użytego FROM. Możemy też nazwać budowany kontener np:

FROM golang:latest as budowniczy

Dzięki temu w COPY możemy podmienić 0 na tą nazwę:

COPY --from=budowniczy /go/zrodla/moje_aplikacje/aplikacja .

Zbudujmy ponownie nasz kontener:

[rm-rf@localhost aplikacja]# docker build -t aplikacja:latest .
Sending build context to Docker daemon 3.584kB
Step 1/10 : FROM golang:latest
 ---> 138bd936fa29
Step 2/10 : MAINTAINER Daniel Wyrzyński
 ---> Running in e709211cb8ca
Removing intermediate container e709211cb8ca
 ---> a18e75d29133
Step 3/10 : WORKDIR /go/zrodla/moje_aplikacje
Removing intermediate container d5c67db01b18
 ---> 50d3c37d437e
Step 4/10 : COPY aplikacja.go .
 ---> e9f0cc3cb231
Step 5/10 : RUN go build -o aplikacja .
 ---> Running in b92b731b4aee
Removing intermediate container b92b731b4aee
 ---> 03636503fc66
Step 6/10 : FROM alpine:latest
latest: Pulling from library/alpine
ff3a5c916c92: Pull complete 
Digest: sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0
Status: Downloaded newer image for alpine:latest
 ---> 3fd9065eaf02
Step 7/10 : RUN apk --no-cache add ca-certificates
 ---> Running in eb2e7a108680 
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz 
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz 
(1/1) Installing ca-certificates (20171114-r0) 
Executing busybox-1.27.2-r7.trigger 
Executing ca-certificates-20171114-r0.trigger 
OK: 5 MiB in 12 packages 
Removing intermediate container eb2e7a108680 
 ---> 9ead9d0fb133 
Step 8/10 : WORKDIR /appki/ 
Removing intermediate container 960390685c7c 
 ---> c3be234375f9 
Step 9/10 : COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja . 
 ---> 500e024d061b 
Step 10/10 : CMD ["./aplikacja"] 
 ---> Running in 0f3c3d917f86 
Removing intermediate container 0f3c3d917f86
 ---> 0d6b033d7e08
Successfully built 0d6b033d7e08
Successfully tagged aplikacja:latest

Jak teraz wygląda nasza kontener?

[rm-rf@localhost aplikacja]# docker images 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
aplikacja           latest              0d6b033d7e08        7 seconds ago       6.54MB

Oprócz tego, że nasz obraz zajmuje 6.54MB to dodatkowo cały „przepis” mamy tylko w jednym pliku Dockerfile, a aplikacja sama trafia do malutkiego kontenera Alpine. Dzięki temu mamy dużą przejrzystość. Wcześniej potrzebowaliśmy dwóch Dockerfile (jeden dla golang, drugi dla alpine) do tego jeśli chcieliśmy to sobie zautomatyzować,  potrzebny był plik ze skryptem w bashu 🙂

Z automatu od kiedy funkcja Multi-Stage Builds pojawiła się w Dockerze trafiła do zasad best practice budowania kontenerów. Myślę, że jest to całkowicie zrozumiałe i nie trzeba nikogo przekonywać jak bardzo jest użyteczna.

Gorąco zachęcam do wprowadzenie jej w życie każdemu entuzjaście Dockera i testowania w ten sposób budowanych kontenerów 🙂