Voordat een functie in productie gaat, is er in deze tijd van complexe orkestrators en CI/CD een lange weg te gaan van commit naar testen en oplevering. Voorheen kon je nieuwe bestanden uploaden via FTP (dat doet niemand meer, toch?) en het “implementatie”-proces duurde enkele seconden. Nu moet u een samenvoegverzoek maken en lang wachten voordat de functie gebruikers bereikt.
Onderdeel van dit pad is het bouwen van een Docker-installatiekopie. Soms duurt de montage minuten, soms tientallen minuten, wat nauwelijks normaal te noemen is. In dit artikel nemen we een eenvoudige applicatie die we in een afbeelding verpakken, passen we verschillende methoden toe om het bouwen te versnellen en kijken we naar de nuances van hoe deze methoden werken.
Wij hebben goede ervaring met het maken en ondersteunen van mediawebsites:
Wij implementeren in GitLab. We verzamelen afbeeldingen, pushen ze naar GitLab Registry en rollen ze uit naar productie. Het langste werk op deze lijst is het samenstellen van afbeeldingen. Bijvoorbeeld: zonder optimalisatie duurde elke backend-build 14 minuten.
Uiteindelijk werd het duidelijk dat we zo niet langer konden leven, en we gingen zitten om erachter te komen waarom het zo lang duurde om de beelden te verzamelen. Hierdoor zijn we erin geslaagd de montagetijd terug te brengen tot 30 seconden!
Laten we voor dit artikel, om niet gebonden te zijn aan de omgeving van Reminder, eens kijken naar een voorbeeld van het samenstellen van een lege Angular-applicatie. Laten we dus onze applicatie maken:
ng n app
Voeg er PWA aan toe (wij zijn vooruitstrevend):
ng add @angular/pwa --project app
Terwijl er een miljoen NPM-pakketten worden gedownload, gaan we uitzoeken hoe de docker-image werkt. Docker biedt de mogelijkheid om applicaties te verpakken en uit te voeren in een geïsoleerde omgeving die een container wordt genoemd. Dankzij isolatie kun je meerdere containers tegelijkertijd op één server draaien. Containers zijn veel lichter dan virtuele machines omdat ze rechtstreeks op de systeemkernel draaien. Om een container met onze applicatie te kunnen draaien, moeten we eerst een image maken waarin we alles verpakken wat nodig is om onze applicatie te laten draaien. In wezen is een afbeelding een kopie van het bestandssysteem. Neem bijvoorbeeld het Dockerbestand:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Een Dockerfile is een set instructies; Door ze allemaal uit te voeren, slaat Docker de wijzigingen in het bestandssysteem op en legt ze over de vorige heen. Elk team creëert zijn eigen laag. En het uiteindelijke beeld bestaat uit lagen die met elkaar zijn gecombineerd.
Wat belangrijk is om te weten: elke Docker-laag kan cachen. Als er niets is veranderd sinds de laatste build, zal de docker in plaats van de opdracht uit te voeren een kant-en-klare laag nemen. Omdat de belangrijkste toename in bouwsnelheid te danken is aan het gebruik van de cache, zullen we bij het meten van de bouwsnelheid specifiek aandacht besteden aan het bouwen van een afbeelding met een kant-en-klare cache. Dus stap voor stap:
- We verwijderen de afbeeldingen lokaal zodat eerdere runs geen invloed hebben op de test.
docker rmi $(docker images -q)
- We lanceren de build voor de eerste keer.
time docker build -t app .
- We veranderen het bestand src/index.html - we imiteren het werk van een programmeur.
- We voeren de build een tweede keer uit.
time docker build -t app .
Als de omgeving voor het bouwen van images correct is geconfigureerd (meer daarover hieronder), zal Docker bij het starten van de build al een aantal caches aan boord hebben. Onze taak is om te leren hoe we de cache kunnen gebruiken, zodat de build zo snel mogelijk verloopt. Omdat we ervan uitgaan dat het uitvoeren van een build zonder cache maar één keer gebeurt (de allereerste keer), kunnen we negeren hoe langzaam die eerste keer was. Bij tests is de tweede run van de build belangrijk voor ons, wanneer de caches al zijn opgewarmd en we klaar zijn om onze taart te bakken. Sommige tips hebben echter ook invloed op de eerste build.
Laten we het hierboven beschreven Dockerbestand in de projectmap plaatsen en de build starten. Alle lijsten zijn gecomprimeerd voor leesgemak.
$ 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
We veranderen de inhoud van src/index.html en voeren het een tweede keer uit.
$ 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
Om te zien of we de afbeelding hebben, voert u de opdracht uit docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
Voordat het gaat bouwen, neemt docker alle bestanden in de huidige context en stuurt ze naar de daemon Sending build context to Docker daemon 409MB
. De build-context wordt opgegeven als het laatste argument voor de build-opdracht. In ons geval is dit de huidige map - “.”, - en Docker sleept alles wat we in deze map hebben. 409 MB is veel: laten we nadenken over hoe we dit kunnen oplossen.
De context verkleinen
Om de context te verkleinen zijn er twee opties. Of plaats alle bestanden die nodig zijn voor de montage in een aparte map en wijs de docker-context naar deze map. Dit is misschien niet altijd handig, dus het is mogelijk om uitzonderingen te specificeren: wat mag niet in de context worden gesleept. Om dit te doen, plaatst u het .dockerignore-bestand in het project en geeft u aan wat niet nodig is voor de build:
.git
/node_modules
en voer de build opnieuw uit:
$ 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 is veel beter dan 409 MB. We hebben ook de afbeeldingsgrootte verkleind van 1.74 naar 1.38 GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
Laten we proberen de grootte van de afbeelding verder te verkleinen.
Wij gebruiken Alpine
Een andere manier om te besparen op de afbeeldingsgrootte is door een kleine bovenliggende afbeelding te gebruiken. Het ouderbeeld is het beeld op basis waarvan ons beeld wordt voorbereid. De onderste laag wordt gespecificeerd door de opdracht FROM
in Dockerfile. In ons geval gebruiken we een op Ubuntu gebaseerde afbeelding waarop al nodejs is geïnstalleerd. En hij weegt...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... bijna een gigabyte. U kunt het volume aanzienlijk verminderen door een image te gebruiken op basis van Alpine Linux. Alpine is een heel kleine Linux. De docker-image voor nodejs op basis van alpine weegt slechts 88.5 MB. Laten we dus ons levendige imago in de huizen vervangen:
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
We moesten een aantal dingen installeren die nodig zijn om de applicatie te bouwen. Ja, Angular bouwt niet zonder Python ¯(°_o)/¯
Maar de afbeeldingsgrootte daalde naar 150 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
Laten we nog verder gaan.
Meertrapsmontage
Niet alles wat op de afbeelding staat, is wat we nodig hebben in de productie.
$ 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
Met docker run app ls -lah
we hebben een container gelanceerd op basis van ons imago app
en voerde daarin de opdracht uit ls -lah
, waarna de container zijn werk voltooide.
Bij de productie hebben we alleen een map nodig dist
. In dit geval moeten de bestanden op de een of andere manier naar buiten worden gebracht. U kunt een HTTP-server op nodejs draaien. Maar we zullen het makkelijker maken. Raad een Russisch woord met vier letters “y”. Rechts! Ynzhynyksy. Laten we een afbeelding maken met nginx en er een map in plaatsen dist
en een kleine configuratie:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
Een meerfasige opbouw zal ons hierbij helpen. Laten we ons Dockerbestand wijzigen:
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 .
Nu hebben we twee instructies FROM
in de Dockerfile voert elk van hen een andere bouwstap uit. Wij hebben de eerste gebeld builder
, maar vanaf de laatste FROM wordt ons uiteindelijke beeld voorbereid. De laatste stap is het kopiëren van het artefact van onze assemblage in de vorige stap naar de uiteindelijke afbeelding met nginx. De grootte van de afbeelding is aanzienlijk afgenomen:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
Laten we de container met onze afbeelding uitvoeren en ervoor zorgen dat alles werkt:
docker run -p8080:80 app
Met behulp van de optie -p8080:80 hebben we poort 8080 op onze hostmachine doorgestuurd naar poort 80 in de container waar nginx draait. Open in de browser
Door de afbeeldingsgrootte te verkleinen van 1.74 GB naar 36 MB wordt de tijd die nodig is om uw applicatie in productie te nemen aanzienlijk verkort. Maar laten we teruggaan naar de montagetijd.
$ 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
De volgorde van lagen wijzigen
Onze eerste drie stappen zijn in de cache opgeslagen (hint Using cache
). Bij de vierde stap worden alle projectbestanden gekopieerd en bij de vijfde stap worden afhankelijkheden geïnstalleerd RUN npm ci
- maar liefst 47.338s. Waarom afhankelijkheden elke keer opnieuw installeren als ze zeer zelden veranderen? Laten we uitzoeken waarom ze niet in de cache zijn opgeslagen. Het punt is dat Docker laag voor laag controleert of de opdracht en de bijbehorende bestanden zijn gewijzigd. Bij de vierde stap kopiëren we alle bestanden van ons project, en daartussen zijn er natuurlijk wijzigingen, dus Docker haalt niet alleen deze laag niet uit de cache, maar ook alle daaropvolgende! Laten we enkele kleine wijzigingen aanbrengen in het Dockerbestand.
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 .
Eerst worden package.json en package-lock.json gekopieerd, vervolgens worden de afhankelijkheden geïnstalleerd en pas daarna wordt het hele project gekopieerd. Als gevolg:
$ 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 seconden in plaats van 3 minuten - veel beter! De juiste volgorde van de lagen is belangrijk: eerst kopiëren we wat niet verandert, vervolgens wat zelden verandert en ten slotte wat vaak verandert.
Vervolgens een paar woorden over het samenstellen van afbeeldingen in CI/CD-systemen.
Gebruik van eerdere afbeeldingen voor cache
Als we een soort SaaS-oplossing gebruiken voor de build, kan de lokale Docker-cache schoon en fris zijn. Om de havenarbeider een plaats te geven waar hij de gebakken lagen kan ophalen, geeft u hem de eerder gebouwde afbeelding.
Laten we een voorbeeld nemen van het bouwen van onze applicatie in GitHub Actions. Wij gebruiken deze configuratie
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
De afbeelding wordt in twee minuten en 20 seconden samengesteld en naar GitHub-pakketten gepusht:
Laten we nu de build wijzigen zodat een cache wordt gebruikt op basis van eerder gebouwde afbeeldingen:
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
Eerst moeten we u vertellen waarom twee opdrachten worden gestart build
. Feit is dat bij een meertrapsassemblage het resulterende beeld een reeks lagen uit de laatste fase zal zijn. In dit geval worden lagen uit voorgaande lagen niet in de afbeelding opgenomen. Wanneer u de definitieve afbeelding van een eerdere build gebruikt, kan Docker daarom geen kant-en-klare lagen vinden om de afbeelding met nodejs te bouwen (builder-fase). Om dit probleem op te lossen wordt een tussenbeeld gecreëerd $IMAGE_NAME-builder-stage
en wordt naar GitHub-pakketten gepusht, zodat het in een volgende build als cachebron kan worden gebruikt.
De totale montagetijd werd teruggebracht tot anderhalve minuut. Er wordt een halve minuut besteed aan het ophalen van eerdere afbeeldingen.
Voorafbeelding maken
Een andere manier om het probleem van een schone Docker-cache op te lossen, is door enkele lagen naar een ander Docker-bestand te verplaatsen, het afzonderlijk te bouwen, het in de Container Registry te plaatsen en het als ouder te gebruiken.
We creëren onze eigen nodejs-image om een Angular-applicatie te bouwen. Maak Dockerfile.node in het project
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
We verzamelen en pushen een publieke afbeelding in Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest
Nu gebruiken we in ons hoofd Dockerbestand de voltooide afbeelding:
FROM exsmund/node-for-angular:latest as builder
...
In ons voorbeeld is de bouwtijd niet afgenomen, maar vooraf gebouwde afbeeldingen kunnen handig zijn als u veel projecten heeft en in elk ervan dezelfde afhankelijkheden moet installeren.
We hebben verschillende methoden bekeken om het bouwen van docker-images te versnellen. Als u wilt dat de implementatie snel verloopt, kunt u dit in uw project proberen:
- context verkleinen;
- gebruik van kleine ouderafbeeldingen;
- meertrapsmontage;
- het veranderen van de volgorde van instructies in het Dockerfile om efficiënt gebruik te maken van de cache;
- het opzetten van een cache in CI/CD-systemen;
- voorlopige creatie van afbeeldingen.
Ik hoop dat het voorbeeld duidelijker maakt hoe Docker werkt, en dat je je implementatie optimaal kunt configureren. Om met de voorbeelden uit het artikel te spelen, is er een repository gemaakt
Bron: www.habr.com