Bevor ein Feature in die Produktion geht, ist es in Zeiten komplexer Orchestratoren und CI/CD ein langer Weg von der Festschreibung bis hin zu Tests und Auslieferung. Früher konnte man neue Dateien per FTP hochladen (das macht doch niemand mehr, oder?), und der „Bereitstellungsprozess“ dauerte Sekunden. Jetzt müssen Sie eine Zusammenführungsanforderung erstellen und lange warten, bis die Funktion die Benutzer erreicht.
Ein Teil dieses Weges besteht darin, ein Docker-Image zu erstellen. Manchmal dauert die Montage Minuten, manchmal mehrere zehn Minuten, was man kaum als normal bezeichnen kann. In diesem Artikel nehmen wir eine einfache Anwendung, die wir in ein Image packen, wenden mehrere Methoden an, um den Build zu beschleunigen, und schauen uns die Nuancen der Funktionsweise dieser Methoden an.
Wir haben gute Erfahrungen in der Erstellung und Betreuung von Medien-Websites:
Wir stellen es auf GitLab bereit. Wir sammeln Bilder, übertragen sie in die GitLab-Registrierung und führen sie in die Produktion ein. Der längste Teil dieser Liste ist das Zusammenstellen von Bildern. Beispiel: Ohne Optimierung dauerte jeder Backend-Build 14 Minuten.
Am Ende wurde klar, dass wir so nicht länger leben konnten, und wir setzten uns zusammen, um herauszufinden, warum das Sammeln der Bilder so lange dauerte. Dadurch ist es uns gelungen, die Montagezeit auf 30 Sekunden zu reduzieren!
Um nicht an die Umgebung von Reminder gebunden zu sein, schauen wir uns in diesem Artikel ein Beispiel für die Zusammenstellung einer leeren Angular-Anwendung an. Erstellen wir also unsere Anwendung:
ng n app
Fügen Sie PWA hinzu (wir sind fortschrittlich):
ng add @angular/pwa --project app
Während eine Million NPM-Pakete heruntergeladen werden, wollen wir herausfinden, wie das Docker-Image funktioniert. Docker bietet die Möglichkeit, Anwendungen zu paketieren und in einer isolierten Umgebung, einem sogenannten Container, auszuführen. Dank der Isolation können Sie viele Container gleichzeitig auf einem Server ausführen. Container sind viel leichter als virtuelle Maschinen, da sie direkt auf dem Systemkernel ausgeführt werden. Um einen Container mit unserer Anwendung auszuführen, müssen wir zunächst ein Image erstellen, in das wir alles packen, was für die Ausführung unserer Anwendung erforderlich ist. Im Wesentlichen ist ein Image eine Kopie des Dateisystems. Nehmen Sie zum Beispiel die Docker-Datei:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Eine Docker-Datei ist eine Reihe von Anweisungen; Durch die einzelnen Schritte speichert Docker die Änderungen im Dateisystem und überlagert sie mit den vorherigen. Jedes Team erstellt seine eigene Ebene. Und das fertige Bild besteht aus zusammengefügten Ebenen.
Wichtig zu wissen: Jeder Docker-Layer kann zwischenspeichern. Wenn sich seit dem letzten Build nichts geändert hat, führt der Docker den Befehl nicht aus, sondern verwendet eine vorgefertigte Ebene. Da die Steigerung der Build-Geschwindigkeit hauptsächlich auf die Verwendung des Caches zurückzuführen ist, werden wir bei der Messung der Build-Geschwindigkeit besonders darauf achten, ein Image mit einem vorgefertigten Cache zu erstellen. Also, Schritt für Schritt:
- Wir löschen die Bilder lokal, damit frühere Läufe den Test nicht beeinträchtigen.
docker rmi $(docker images -q)
- Wir starten den Build zum ersten Mal.
time docker build -t app .
- Wir ändern die Datei src/index.html – wir ahmen die Arbeit eines Programmierers nach.
- Wir führen den Build ein zweites Mal aus.
time docker build -t app .
Wenn die Umgebung zum Erstellen von Images richtig konfiguriert ist (mehr dazu weiter unten), verfügt Docker zu Beginn des Builds bereits über eine Reihe von Caches. Unsere Aufgabe ist es zu lernen, wie man den Cache nutzt, damit der Build so schnell wie möglich geht. Da wir davon ausgehen, dass die Ausführung eines Builds ohne Cache nur einmal vorkommt – nämlich beim allerersten Mal – können wir ignorieren, wie langsam das erste Mal war. Bei Tests ist uns der zweite Durchlauf des Builds wichtig, wenn die Caches bereits aufgewärmt sind und wir bereit sind, unseren Kuchen zu backen. Einige Tipps wirken sich jedoch auch auf den ersten Build aus.
Legen wir die oben beschriebene Docker-Datei in den Projektordner und starten den Build. Zur besseren Lesbarkeit wurden alle Auflistungen gekürzt.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Downloaded newer image for node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581ms
Successfully built c8c279335f46
Successfully tagged app:latest
real 5m4.541s
user 0m0.000s
sys 0m0.000s
Wir ändern den Inhalt von src/index.html und führen es ein zweites Mal aus.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902ms
Successfully built 79f335df92d3
Successfully tagged app:latest
real 3m33.262s
user 0m0.000s
sys 0m0.000s
Um zu sehen, ob wir das Bild haben, führen Sie den Befehl aus docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
Vor dem Erstellen nimmt Docker alle Dateien im aktuellen Kontext und sendet sie an seinen Daemon Sending build context to Docker daemon 409MB
. Der Build-Kontext wird als letztes Argument für den Build-Befehl angegeben. In unserem Fall ist dies das aktuelle Verzeichnis – „.“ – und Docker zieht alles, was wir in diesem Ordner haben. 409 MB sind viel: Lassen Sie uns darüber nachdenken, wie wir das Problem beheben können.
Den Kontext reduzieren
Um den Kontext zu reduzieren, gibt es zwei Möglichkeiten. Oder legen Sie alle für die Montage benötigten Dateien in einem separaten Ordner ab und verweisen Sie den Docker-Kontext auf diesen Ordner. Dies ist möglicherweise nicht immer praktisch, daher ist es möglich, Ausnahmen anzugeben: was nicht in den Kontext gezogen werden soll. Fügen Sie dazu die .dockerignore-Datei in das Projekt ein und geben Sie an, was für den Build nicht benötigt wird:
.git
/node_modules
und führen Sie den Build erneut aus:
$ time docker build -t app .
Sending build context to Docker daemon 607.2kB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313ms
Successfully built 4942f010792a
Successfully tagged app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s
607.2 KB sind viel besser als 409 MB. Außerdem haben wir die Bildgröße von 1.74 auf 1.38 GB reduziert:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
Versuchen wir, die Bildgröße weiter zu reduzieren.
Wir verwenden Alpine
Eine weitere Möglichkeit, Bildgröße zu sparen, besteht darin, ein kleines übergeordnetes Bild zu verwenden. Das Elternbild ist das Bild, auf dessen Grundlage unser Bild erstellt wird. Die unterste Ebene wird durch den Befehl angegeben FROM
in Dockerfile. In unserem Fall verwenden wir ein Ubuntu-basiertes Image, auf dem NodeJS bereits installiert ist. Und es wiegt...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... fast ein Gigabyte. Sie können die Lautstärke deutlich reduzieren, indem Sie ein Image auf Basis von Alpine Linux verwenden. Alpine ist ein sehr kleines Linux. Das Docker-Image für NodeJS auf Basis von Alpine wiegt nur 88.5 MB. Ersetzen wir also unser lebendiges Bild in den Häusern:
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Wir mussten einige Dinge installieren, die zum Erstellen der Anwendung erforderlich sind. Ja, Angular kann nicht ohne Python ¯(°_o)/¯ erstellt werden
Aber die Bildgröße ist auf 150 MB gesunken:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
Gehen wir noch weiter.
Mehrstufige Montage
Nicht alles, was im Bild ist, ist das, was wir in der Produktion brauchen.
$ docker run app ls -lah
total 576K
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 .
drwxr-xr-x 1 root root 4.0K Apr 16 20:00 ..
-rwxr-xr-x 1 root root 19 Apr 17 2020 .dockerignore
-rwxr-xr-x 1 root root 246 Apr 17 2020 .editorconfig
-rwxr-xr-x 1 root root 631 Apr 17 2020 .gitignore
-rwxr-xr-x 1 root root 181 Apr 17 2020 Dockerfile
-rwxr-xr-x 1 root root 1020 Apr 17 2020 README.md
-rwxr-xr-x 1 root root 3.6K Apr 17 2020 angular.json
-rwxr-xr-x 1 root root 429 Apr 17 2020 browserslist
drwxr-xr-x 3 root root 4.0K Apr 16 19:54 dist
drwxr-xr-x 3 root root 4.0K Apr 17 2020 e2e
-rwxr-xr-x 1 root root 1015 Apr 17 2020 karma.conf.js
-rwxr-xr-x 1 root root 620 Apr 17 2020 ngsw-config.json
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 node_modules
-rwxr-xr-x 1 root root 494.9K Apr 17 2020 package-lock.json
-rwxr-xr-x 1 root root 1.3K Apr 17 2020 package.json
drwxr-xr-x 5 root root 4.0K Apr 17 2020 src
-rwxr-xr-x 1 root root 210 Apr 17 2020 tsconfig.app.json
-rwxr-xr-x 1 root root 489 Apr 17 2020 tsconfig.json
-rwxr-xr-x 1 root root 270 Apr 17 2020 tsconfig.spec.json
-rwxr-xr-x 1 root root 1.9K Apr 17 2020 tslint.json
Mit docker run app ls -lah
Wir haben einen Container basierend auf unserem Image gestartet app
und den darin enthaltenen Befehl ausgeführt ls -lah
, woraufhin der Container seine Arbeit beendete.
In der Produktion benötigen wir lediglich einen Ordner dist
. In diesem Fall müssen die Dateien irgendwie nach außen gegeben werden. Sie können einen HTTP-Server auf NodeJS ausführen. Aber wir machen es einfacher. Erraten Sie ein russisches Wort mit den vier Buchstaben „y“. Rechts! Ynzhynyksy. Nehmen wir ein Bild mit Nginx auf und legen einen Ordner darin ab dist
und eine kleine Konfiguration:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
Der mehrstufige Aufbau wird uns dabei helfen. Ändern wir unsere Docker-Datei:
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
Jetzt haben wir zwei Anweisungen FROM
In der Docker-Datei führt jeder von ihnen einen anderen Build-Schritt aus. Wir haben den ersten angerufen builder
, aber beginnend mit dem letzten FROM wird unser endgültiges Bild vorbereitet. Der letzte Schritt besteht darin, das Artefakt unserer Assembly im vorherigen Schritt mit Nginx in das endgültige Image zu kopieren. Die Größe des Bildes hat sich deutlich verringert:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
Lassen Sie uns den Container mit unserem Image ausführen und sicherstellen, dass alles funktioniert:
docker run -p8080:80 app
Mit der Option -p8080:80 haben wir Port 8080 auf unserem Host-Computer an Port 80 innerhalb des Containers weitergeleitet, in dem Nginx ausgeführt wird. Im Browser öffnen
Durch die Reduzierung der Bildgröße von 1.74 GB auf 36 MB verkürzt sich die Zeit, die für die Bereitstellung Ihrer Anwendung in der Produktion benötigt wird, erheblich. Doch zurück zur Montagezeit.
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/11 : FROM node:12.16.2-alpine3.11 as builder
Step 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/11 : WORKDIR /app
---> Using cache
Step 4/11 : COPY . .
Step 5/11 : RUN npm ci
added 1357 packages in 47.338s
Step 6/11 : RUN npm run build --prod
Date: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms
---> 27f1479221e4
Step 7/11 : FROM nginx:stable-alpine
Step 8/11 : WORKDIR /app
---> Using cache
Step 9/11 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 11/11 : COPY --from=builder /app/dist/app .
Successfully built d201471c91ad
Successfully tagged app:latest
real 2m17.700s
user 0m0.000s
sys 0m0.000s
Ändern der Reihenfolge der Ebenen
Unsere ersten drei Schritte wurden zwischengespeichert (Hinweis Using cache
). Im vierten Schritt werden alle Projektdateien kopiert und im fünften Schritt werden Abhängigkeiten installiert RUN npm ci
- bis zu 47.338 Sekunden. Warum Abhängigkeiten jedes Mal neu installieren, wenn sie sich nur sehr selten ändern? Lassen Sie uns herausfinden, warum sie nicht zwischengespeichert wurden. Der Punkt ist, dass Docker Schicht für Schicht prüft, ob sich der Befehl und die damit verbundenen Dateien geändert haben. Im vierten Schritt kopieren wir alle Dateien unseres Projekts, und darunter gibt es natürlich auch Änderungen, sodass Docker nicht nur diese Ebene nicht aus dem Cache nimmt, sondern auch alle nachfolgenden! Nehmen wir einige kleine Änderungen an der Docker-Datei vor.
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
Zuerst werden package.json und package-lock.json kopiert, dann werden Abhängigkeiten installiert und erst danach wird das gesamte Projekt kopiert. Ergebend:
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/12 : FROM node:12.16.2-alpine3.11 as builder
Step 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/12 : WORKDIR /app
---> Using cache
Step 4/12 : COPY package*.json ./
---> Using cache
Step 5/12 : RUN npm ci
---> Using cache
Step 6/12 : COPY . .
Step 7/12 : RUN npm run build --prod
Date: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms
---> 1b9448c73558
Step 8/12 : FROM nginx:stable-alpine
Step 9/12 : WORKDIR /app
---> Using cache
Step 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 12/12 : COPY --from=builder /app/dist/app .
Successfully built a44dd7c217c3
Successfully tagged app:latest
real 0m46.497s
user 0m0.000s
sys 0m0.000s
46 Sekunden statt 3 Minuten – viel besser! Die richtige Reihenfolge der Ebenen ist wichtig: Zuerst kopieren wir, was sich nicht ändert, dann, was sich selten ändert und schließlich, was sich häufig ändert.
Als nächstes ein paar Worte zum Zusammenstellen von Bildern in CI/CD-Systemen.
Vorherige Bilder für den Cache verwenden
Wenn wir für den Build eine Art SaaS-Lösung verwenden, ist der lokale Docker-Cache möglicherweise sauber und frisch. Um dem Docker einen Platz für die gebackenen Schichten zu geben, geben Sie ihm das zuvor erstellte Image.
Nehmen wir ein Beispiel für die Erstellung unserer Anwendung in GitHub Actions. Wir verwenden diese Konfiguration
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Build
run: |
docker build
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
Das Bild wird in zwei Minuten und 20 Sekunden zusammengestellt und an GitHub-Pakete übertragen:
Jetzt ändern wir den Build so, dass ein Cache basierend auf zuvor erstellten Images verwendet wird:
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Pull latest images
run: |
docker pull $IMAGE_NAME:latest || true
docker pull $IMAGE_NAME-builder-stage:latest || true
- name: Images list
run: |
docker images
- name: Build
run: |
docker build
--target builder
--cache-from $IMAGE_NAME-builder-stage:latest
-t $IMAGE_NAME-builder-stage
.
docker build
--cache-from $IMAGE_NAME-builder-stage:latest
--cache-from $IMAGE_NAME:latest
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME-builder-stage:latest
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
Zuerst müssen wir Ihnen erklären, warum zwei Befehle gestartet werden build
. Tatsache ist, dass das resultierende Bild bei einer mehrstufigen Montage aus einer Reihe von Ebenen aus der letzten Stufe besteht. In diesem Fall werden Ebenen aus vorherigen Ebenen nicht in das Bild einbezogen. Wenn Docker daher das endgültige Image aus einem früheren Build verwendet, kann es keine fertigen Ebenen finden, um das Image mit NodeJS zu erstellen (Builder-Stufe). Um dieses Problem zu lösen, wird ein Zwischenbild erstellt $IMAGE_NAME-builder-stage
und wird an GitHub-Pakete gepusht, damit es in einem nachfolgenden Build als Cache-Quelle verwendet werden kann.
Die Gesamtmontagezeit wurde auf eineinhalb Minuten verkürzt. Eine halbe Minute wird damit verbracht, frühere Bilder aufzurufen.
Vorabbild
Eine andere Möglichkeit, das Problem eines sauberen Docker-Cache zu lösen, besteht darin, einige der Ebenen in eine andere Docker-Datei zu verschieben, sie separat zu erstellen, sie in die Container Registry zu übertragen und als übergeordnete Datei zu verwenden.
Wir erstellen unser eigenes NodeJS-Image, um eine Angular-Anwendung zu erstellen. Erstellen Sie Dockerfile.node im Projekt
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
Wir sammeln und veröffentlichen ein öffentliches Bild im Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest
Jetzt verwenden wir in unserer Haupt-Docker-Datei das fertige Image:
FROM exsmund/node-for-angular:latest as builder
...
In unserem Beispiel hat sich die Erstellungszeit nicht verkürzt, aber vorgefertigte Images können nützlich sein, wenn Sie viele Projekte haben und in jedem von ihnen dieselben Abhängigkeiten installieren müssen.
Wir haben uns verschiedene Methoden angesehen, um die Erstellung von Docker-Images zu beschleunigen. Wenn Sie möchten, dass die Bereitstellung schnell vonstatten geht, verwenden Sie Folgendes in Ihrem Projekt:
- Kontext reduzieren;
- Verwendung kleiner Elternbilder;
- mehrstufige Montage;
- Ändern der Reihenfolge der Anweisungen in der Docker-Datei, um den Cache effizient zu nutzen;
- Einrichten eines Caches in CI/CD-Systemen;
- Vorläufige Erstellung von Bildern.
Ich hoffe, dass das Beispiel die Funktionsweise von Docker deutlicher macht und Sie Ihr Deployment optimal konfigurieren können. Um mit den Beispielen aus dem Artikel zu spielen, wurde ein Repository erstellt
Source: habr.com