Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

Voordat een functie in productie komt, moet het in deze tijden van complexe orkestrators en CI/CD een lange weg afleggen van commit tot tests en levering. Vroeger kon je nieuwe bestanden via FTP uploaden (dat doet tegenwoordig echt niemand meer, toch?) en het 'deployen' duurde seconden. Nu moet je een merge-request aanmaken en een aanzienlijke tijd wachten voordat de functie bij de gebruikers aankomt.

Een deel van deze weg is de bouw van de Docker-afbeelding. Soms duurt de bouw minuten, soms zelfs tientallen minuten, wat moeilijk als normaal te beschouwen is. In dit artikel nemen we een eenvoudige applicatie die we in een afbeelding verpakt, passen we een paar methoden toe om de bouw te versnellen en bespreken we de nuances van deze methoden.

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

We hebben een goede ervaring met het maken en onderhouden van nieuwswebsites: TASS, The Bell, "Nieuwe Krant", Republic… Niet zo lang geleden hebben we ons portfolio uitgebreid door de livegang van de site Reminder. En terwijl we snel nieuwe functies implementeerden en oude bugs oplosten, werd de trage deploy een groot probleem.

We doen de deploy op GitLab. We bouwen de afbeeldingen, pushen ze naar de GitLab Registry en rollen ze uit in productie. Het langste gedeelte in deze lijst is de bouw van de afbeeldingen. Ter illustratie: zonder optimalisatie duurde elke backend-bouw 14 minuten.

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

Uiteindelijk werd duidelijk dat we zo niet verder konden, en we gingen ons verdiepen in waarom de afbeeldingen zoveel tijd nodig hadden voor de bouw. Uiteindelijk is het gelukt om de bouwtijd tot 30 seconden te verkorten!

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

Voor dit artikel, om ons niet aan de omgeving van Reminder te binden, bekijken we een voorbeeld van het bouwen van een lege applicatie op Angular. Dus, laten we onze applicatie maken:

ng n app

We voegen PWA toe (we zijn tenslotte progressief):

ng add @angular/pwa --project app

Terwijl miljoenen npm-pakketten worden gedownload, laten we eens kijken hoe een docker-afbeelding is opgebouwd. Docker biedt de mogelijkheid om applicaties te verpakken en in een geïsoleerde omgeving, die container wordt genoemd, uit te voeren. Dankzij deze isolatie kunnen veel containers tegelijkertijd op één server draaien. Containers zijn aanzienlijk lichter dan virtuele machines, omdat ze rechtstreeks op de systeemkern draaien. Om een container met onze applicatie te starten, moeten we eerst een afbeelding maken waarin we alles verpakken wat nodig is voor onze applicatie om te functioneren. In wezen is een afbeelding een afdruk van het bestandssysteem. Laten we bijvoorbeeld een Dockerfile nemen:

FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod

Dockerfile is een set instructies; wanneer elk ervan wordt uitgevoerd, zal Docker de wijzigingen in het bestandssysteem opslaan en deze toepassen op de vorige. Elke opdracht creëert zijn eigen laag. Een kant-en-klaar image is een combinatie van deze lagen.

Belangrijk om te weten: elke laag kan gecacheerd worden door Docker. Als er niets is veranderd sinds de vorige build, dan zal Docker in plaats van de opdracht uit te voeren de reeds bestaande laag gebruiken. Aangezien de grootste winst in bouwsnelheid zal komen door het gebruik van de cache, zullen we bij het meten van de bouwsnelheid specifiek letten op de build met de opgebouwde cache. Laten we de stappen doornemen:

  1. We verwijderen lokaal de images, zodat eerdere runs geen invloed hebben op de test.
    docker rmi $(docker images -q)
  2. We starten de build voor de eerste keer.
    time docker build -t app .
  3. We wijzigen het bestand src/index.html - we simuleren het werk van een programmeur.
  4. We starten de build voor de tweede keer.
    time docker build -t app .

Als de omgeving voor het bouwen van images goed is ingesteld (waarover iets verderop meer), dan zal Docker bij het starten van de build al een aantal caches hebben. Onze taak is om te leren de cache zo te gebruiken dat de build zo snel mogelijk gaat. Aangezien we aannemen dat de build zonder cache maar één keer plaatsvindt — de eerste keer — kunnen we negeren hoe langzaam deze eerste keer was. In onze tests is de tweede build belangrijk, wanneer de caches al warm zijn en we klaar zijn om onze taart te bakken. Desondanks zullen sommige tips ook invloed hebben op de eerste build.

We plaatsen de hierboven beschreven Dockerfile in de projectmap en starten de build. Alle gegeven listings zijn ingekort voor een betere leesbaarheid.

$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Nieuwere image gedownload voor node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
1357 packages toegevoegd in 22.47s
Step 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37581ms
Succesvol gebouwd c8c279335f46
Succesvol getagd app:latest

real 5m4.541s
user 0m0.000s
sys 0m0.000s

We wijzigen de inhoud van src/index.html en voeren de build voor de 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
 ---> Gebruik makend van cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
1357 packages toegevoegd in 22.47s
Step 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37902ms
Succesvol gebouwd 79f335df92d3
Succesvol getagd app:latest

real 3m33.262s
user 0m0.000s
sys 0m0.000s

Om te zien of we onze image hebben gekregen, voeren we de opdracht uit docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   Ongeveer een minuut geleden   1.74GB

Voordat Docker bouwt, neemt het alle bestanden in de huidige context en stuurt deze naar zijn daemon. Bouwcontext verzenden naar Docker daemon 409MB. De context voor de bouw wordt opgegeven als het laatste argument van het commando build. In ons geval is dit de huidige directory — «.», — en Docker sleurt alles mee wat in deze map staat. 409 MB is veel: laten we nadenken over hoe we dit kunnen verhelpen.

We verkleinen de context.

Om de context te verkleinen, zijn er twee opties. Ofwel plaatsen we alle bestanden die nodig zijn voor de bouw in een aparte map en geven we deze map aan Docker als context. Dit kan soms onhandig zijn, daarom is er de mogelijkheid om uitzonderingen op te geven: wat niet mee moet worden genomen in de context. Hiervoor plaatsen we een bestand .dockerignore in het project en geven we op wat niet nodig is voor de bouw:

.git
/node_modules

en starten we de bouw opnieuw:

$ time docker build -t app .
Bouwcontext verzenden naar Docker daemon 607.2kB
Stap 1/5 : VAN node:12.16.2
Stap 2/5 : WORKDIR /app
 ---> Cache gebruiken
Stap 3/5 : KOPIEER . .
Stap 4/5 : RUN npm ci
1357 pakketten toegevoegd in 22.47s
Stap 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37313ms
Met succes gebouwd 4942f010792a
Met succes getagd app:latest

real 1m47.763s
user 0m0.000s
sys 0m0.000s

607.2 Kbyte — veel beter dan 409 MB. En bovendien hebben we de grootte van het image verminderd van 1.74 naar 1.38GB:

REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
app          latest   4942f010792a   3 minuten geleden   1.38GB

Laten we proberen om de grootte van het image verder te verminderen.

We gebruiken Alpine.

Een andere manier om te besparen op de grootte van het image is het gebruik van een klein ouder image. Het ouder image is de basis waarop ons image is gebouwd. De onderste laag wordt opgegeven met het commando FROM in de Dockerfile. In ons geval gebruiken we een image gebaseerd op Ubuntu, dat al nodejs bevat. En het weegt ...

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minuten geleden 916MB

… bijna een gigabyte. We kunnen de grootte aanzienlijk verminderen door een image op basis van Alpine Linux te gebruiken. Alpine is een zeer kleine Linux-distributie. De Docker-image voor nodejs op basis van Alpine weegt slechts 88.5 MB. Laten we daarom ons zware image 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 paar dingen installeren die nodig zijn voor het bouwen van de applicatie. Ja, Angular bouwt niet zonder Python ¯(°_o)\/¯

Maar de grootte van het image is met 150 MB verminderd:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   aa031edc315a   22 minuten geleden   761MB

Laten we nog een stap verder gaan.

Multi-stage build.

Niet alles wat in het image zit, is nodig voor onze 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 gestart op basis van ons image app en hebben daar een commando in uitgevoerd ls -lah, waarna de container zijn werk beëindigde.

Op productie hebben we alleen de map dist. De bestanden moeten op de een of andere manier extern worden aangeboden. We kunnen een HTTP-server op nodejs starten. Maar we doen het eenvoudiger. Raad het Russische woord dat vier letters 'ы' heeft. Juist! Ынжыныксы. Laten we een image pakken met nginx en deze map toevoegen 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;
    }
}

Dit alles helpt ons met multi-stage build. Laten we ons Dockerfile aanpassen:

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 het Dockerfile, elk voert zijn eigen bouwfase uit. De eerste hebben we genoemd , bedoeld voor het samenstellen van pakketten en distributies. Voor aarch64 is een live-variant samengesteld, gelijk aan x86_64; voor beide architecturen zijn rootfs-afbeeldingen voor RPi4 en qemu-afbeeldingen samengesteld., maar vanaf de laatste FROM zal ons eindimage worden voorbereid. In de laatste stap kopiëren we het artifact van onze build in de vorige fase naar het eindimage met nginx. De grootte van het image is aanzienlijk verminderd:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   2c6c5da07802   29 minutes ago   36MB

Laten we de container met ons image starten en bevestigen dat alles werkt:

docker run -p8080:80 app

Met de optie -p8080:80 hebben we poort 8080 op onze hostmachine doorgeschakeld naar poort 80 binnen de container, waar nginx draait. We openen in de browser http://localhost:8080/ en zien onze applicatie. Alles werkt!

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

Het verminderen van de grootte van het image van 1,74 GB naar 36 MB verkort aanzienlijk de tijd die nodig is om uw applicatie in productie te brengen. Maar laten we teruggaan naar de bouwtijd.

$ 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

We veranderen de volgorde van de lagen

De eerste drie stappen waren gecached (hint Using cache). In de vierde stap worden alle projectbestanden gekopieerd en in de vijfde stap worden de afhankelijkheden geïnstalleerd RUN npm ci — in totaal 47,338 seconden. Waarom elke keer opnieuw afhankelijkheden installeren, als ze zelden veranderen? Laten we eens kijken waarom ze niet gecached zijn. Het komt doordat Docker laag voor laag controleert of de commando's en de bijbehorende bestanden zijn gewijzigd. In de vierde stap kopiëren we alle bestanden van ons project, en daar zijn natuurlijk wijzigingen bij, dus Docker haalt niet alleen deze laag niet uit de cache, maar ook alle daaropvolgende! Laten we een paar kleine wijzigingen aanbrengen in het Dockerfile.

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, daarna worden de afhankelijkheden geïnstalleerd, en pas daarna wordt het volledige project gekopieerd. Als resultaat:

$ time docker build -t app .
Verzendt build context naar Docker daemon 608.8kB
Stap 1/12 : FROM node:12.16.2-alpine3.11 as builder
Stap 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
 ---> Using cache
Stap 3/12 : WORKDIR /app
 ---> Using cache
Stap 4/12 : COPY package*.json ./
 ---> Using cache
Stap 5/12 : RUN npm ci
 ---> Using cache
Stap 6/12 : COPY . .
Stap 7/12 : RUN npm run build --prod
Datum: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 38287ms
 ---> 1b9448c73558
Stap 8/12 : FROM nginx:stable-alpine
Stap 9/12 : WORKDIR /app
 ---> Using cache
Stap 10/12 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Using cache
Stap 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
 ---> Using cache
Stap 12/12 : COPY --from=builder /app/dist/app .
Met succes gebouwd a44dd7c217c3
Met succes gelabeld app:latest

reëel 0m46.497s
gebruiker 0m0.000s
sys 0m0.000s

46 seconden in plaats van 3 minuten — aanzienlijk beter! De juiste volgorde van lagen is belangrijk: eerst kopiëren we wat niet verandert, dan wat zelden verandert, en tenslotte wat vaak verandert.

Vervolgens een paar woorden over het bouwen van images in CI/CD-systemen.

Het gebruik van eerdere images voor caching

Als we een SaaS-oplossing gebruiken voor de bouw, kan de lokale Docker-cache schoon en fris zijn. Om Docker een plek te geven om gebakken lagen van te nemen, geef hem de vorige gebouwde afbeelding.

Laten we als voorbeeld de bouw van onze applicatie in GitHub Actions bekijken. We gebruiken zo'n 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

Het beeld wordt binnen twee minuten en 20 seconden opgebouwd en gepusht naar GitHub Packages:

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

Laten we nu de build wijzigen, zodat de cache op basis van eerder gebouwde images wordt gebruikt:

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 uitleggen waarom er twee opdrachten worden uitgevoerd. buildDit komt omdat in een multi-stage build de resulterende image een set lagen van de laatste stage zal zijn. De lagen van de vorige stages worden echter niet in de image opgenomen. Daarom kan Docker bij gebruik van de finale image uit de vorige build geen gereed lagen vinden voor het bouwen van de image met nodejs (builder stage). Om dit probleem op te lossen, wordt er een tussen-image gemaakt. $IMAGE_NAME-builder-stage en deze wordt naar GitHub Packages gepusht, zodat deze in de volgende build als cachebron kan worden gebruikt.

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

De totale bouwtijd is verminderd tot anderhalve minuut. Dertig seconden worden besteed aan het ophalen van eerdere images.

Voorlopig bouwen van images

Een andere manier om het probleem van een schone Docker-cache op te lossen, is door delen van de lagen naar een andere Dockerfile te verplaatsen, deze apart te bouwen, naar de Container Registry te pushen en als basis te gebruiken.

We maken onze eigen Node.js-image voor het bouwen van een Angular-applicatie. Maak een Dockerfile.node in het project.

FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++

Bouw en push een openbaar image naar Docker Hub:

docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest

Nu gebruiken we het kant-en-klare image in onze hoofd Dockerfile:

FROM exsmund/node-for-angular:latest as builder
...

In ons voorbeeld is de bouwtijd niet verkort, maar vooraf gebouwde images kunnen nuttig zijn als je veel projecten hebt en in elk dezelfde afhankelijkheden moet installeren.

Enkele tips om de bouw van Docker-afbeeldingen te versnellen. Bijvoorbeeld, tot 30 seconden.

We hebben verschillende methoden bekeken om het bouwen van Docker-images te versnellen. Als je wilt dat de deployment snel verloopt, probeer dan in je project het volgende:

  • de context te verkleinen;
  • kleine basis-images te gebruiken;
  • multi-stage builds;
  • de volgorde van instructies in de Dockerfile te wijzigen om de cache efficiënt te gebruiken;
  • de cache in CI/CD-systemen in te stellen;
  • vooraf gebouwde images.

Ik hoop dat het voorbeeld duidelijk maakt hoe Docker werkt en dat je je deployment optimaal kunt instellen. Voor degenen die met de voorbeelden uit het artikel willen experimenteren, is er een repository aangemaakt. https://github.com/devopsprodigy/test-docker-build.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster