Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Før en funksjon kommer i produksjon, i disse dager med komplekse orkestratorer og CI/CD, er det en lang vei å gå fra forpliktelse til tester og levering. Tidligere kunne du laste opp nye filer via FTP (ingen gjør det lenger, ikke sant?), og "distribusjonsprosessen" tok sekunder. Nå må du opprette en sammenslåingsforespørsel og vente lenge på at funksjonen skal nå brukere.

En del av denne banen er å bygge et Docker-bilde. Noen ganger varer forsamlingen minutter, noen ganger titalls minutter, noe som vanskelig kan kalles normalt. I denne artikkelen tar vi en enkel applikasjon som vi pakker inn i et bilde, bruker flere metoder for å få fart på byggingen og ser på nyansene i hvordan disse metodene fungerer.

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Vi har god erfaring med å lage og støtte medienettsteder: TASS, Bell, "Ny avis", Republic… For ikke lenge siden utvidet vi porteføljen vår ved å lansere et produktnettsted påminnelse. Og mens nye funksjoner raskt ble lagt til og gamle feil ble fikset, ble langsom distribusjon et stort problem.

Vi distribuerer til GitLab. Vi samler inn bilder, skyver dem til GitLab Registry og ruller dem ut til produksjon. Det lengste på denne listen er å sette sammen bilder. For eksempel: uten optimalisering tok hver backend-bygging 14 minutter.

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Til slutt ble det klart at vi ikke lenger kunne leve slik, og vi satte oss ned for å finne ut hvorfor bildene tok så lang tid å samle. Som et resultat klarte vi å redusere monteringstiden til 30 sekunder!

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

For denne artikkelen, for ikke å være knyttet til Reminders miljø, la oss se på et eksempel på å sette sammen en tom Angular-applikasjon. Så la oss lage vår applikasjon:

ng n app

Legg til PWA til den (vi er progressive):

ng add @angular/pwa --project app

Mens en million npm-pakker lastes ned, la oss finne ut hvordan docker-bildet fungerer. Docker gir muligheten til å pakke applikasjoner og kjøre dem i et isolert miljø kalt en container. Takket være isolasjon kan du kjøre mange containere samtidig på én server. Beholdere er mye lettere enn virtuelle maskiner fordi de kjører direkte på systemkjernen. For å kjøre en container med applikasjonen vår, må vi først lage et bilde der vi pakker alt som er nødvendig for at applikasjonen vår skal kjøre. I hovedsak er et bilde en kopi av filsystemet. Ta for eksempel Dockerfilen:

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

En Dockerfile er et sett med instruksjoner; Ved å gjøre hver av dem, vil Docker lagre endringene i filsystemet og legge dem på de forrige. Hvert lag lager sitt eget lag. Og det ferdige bildet er lag kombinert sammen.

Hva er viktig å vite: hvert Docker-lag kan cache. Hvis ingenting har endret seg siden forrige bygging, vil docker ta et ferdig lag i stedet for å utføre kommandoen. Siden hovedøkningen i byggehastighet vil skyldes bruken av cachen, vil vi ved måling av byggehastighet være spesielt oppmerksomme på å bygge et bilde med en ferdig cache. Så, steg for steg:

  1. Vi sletter bildene lokalt slik at tidligere kjøringer ikke påvirker testen.
    docker rmi $(docker images -q)
  2. Vi lanserer bygget for første gang.
    time docker build -t app .
  3. Vi endrer filen src/index.html - vi imiterer arbeidet til en programmerer.
  4. Vi kjører bygget en gang til.
    time docker build -t app .

Hvis miljøet for å bygge bilder er riktig konfigurert (mer om det nedenfor), vil Docker allerede ha en haug med cacher om bord når byggingen starter. Vår oppgave er å lære å bruke cachen slik at byggingen går så raskt som mulig. Siden vi antar at å kjøre en build uten cache bare skjer én gang – den aller første gangen – kan vi derfor ignorere hvor treg den første gangen var. I tester er den andre kjøringen av bygget viktig for oss, når cachene allerede er varmet opp og vi er klare til å bake kaken vår. Noen tips vil imidlertid også påvirke det første bygget.

La oss legge Dockerfilen beskrevet ovenfor i prosjektmappen og starte byggingen. Alle oppføringer er komprimert for enkel lesing.

$ 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

Vi endrer innholdet i src/index.html og kjører det en gang til.

$ 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

For å se om vi har bildet, kjør kommandoen docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   About a minute ago   1.74GB

Før du bygger, tar docker alle filene i gjeldende kontekst og sender dem til sin daemon Sending build context to Docker daemon 409MB. Byggkonteksten er spesifisert som det siste argumentet til byggkommandoen. I vårt tilfelle er dette gjeldende katalog - ".", - og Docker drar alt vi har i denne mappen. 409 MB er mye: la oss tenke på hvordan vi fikser det.

Redusere konteksten

For å redusere konteksten er det to alternativer. Eller legg alle filene som trengs for montering i en egen mappe og pek docker-konteksten til denne mappen. Dette er kanskje ikke alltid praktisk, så det er mulig å spesifisere unntak: hva som ikke skal dras inn i konteksten. For å gjøre dette, legg inn .dockerignore-filen i prosjektet og angi hva som ikke er nødvendig for bygget:

.git
/node_modules

og kjør byggingen på nytt:

$ 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 er mye bedre enn 409 MB. Vi reduserte også bildestørrelsen fra 1.74 til 1.38 GB:

REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
app          latest   4942f010792a   3 minutes ago   1.38GB

La oss prøve å redusere størrelsen på bildet ytterligere.

Vi bruker Alpine

En annen måte å spare på bildestørrelsen er å bruke et lite overordnet bilde. Foreldrebildet er bildet som vårt bilde er utarbeidet på grunnlag av. Det nederste laget er spesifisert av kommandoen FROM i Dockerfile. I vårt tilfelle bruker vi et Ubuntu-basert bilde som allerede har nodejs installert. Og den veier...

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB

... nesten en gigabyte. Du kan redusere volumet betraktelig ved å bruke et bilde basert på Alpine Linux. Alpine er en veldig liten Linux. Docker-bildet for nodejs basert på alpin veier bare 88.5 MB. Så la oss erstatte vårt livlige bilde i husene:

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

Vi måtte installere noen ting som er nødvendige for å bygge applikasjonen. Ja, Angular bygger ikke uten Python ¯(°_o)/¯

Men bildestørrelsen falt til 150 MB:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   aa031edc315a   22 minutes ago   761MB

La oss gå enda lenger.

Flertrinns montering

Ikke alt som er i bildet er det vi trenger i produksjonen.

$ 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

Med docker run app ls -lah vi lanserte en container basert på bildet vårt app og utførte kommandoen i den ls -lah, hvoretter containeren fullførte arbeidet.

I produksjonen trenger vi kun en mappe dist. I dette tilfellet må filene på en eller annen måte gis utenfor. Du kan kjøre noen HTTP-servere på nodejs. Men vi skal gjøre det lettere. Gjett et russisk ord som har fire bokstaver "y". Ikke sant! Ynzhynyksy. La oss ta et bilde med nginx, legge inn en mappe i det dist og en liten konfigurasjon:

server {
    listen 80 default_server;
    server_name localhost;
    charset utf-8;
    root /app/dist;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Flertrinnsbygging vil hjelpe oss å gjøre alt dette. La oss endre Dockerfilen vår:

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 .

Nå har vi to instruksjoner FROM i Dockerfilen kjører hver av dem et annet byggetrinn. Vi ringte den første builder, men fra den siste FROM, vil vårt endelige bilde bli forberedt. Det siste trinnet er å kopiere artefakten til sammenstillingen vår i forrige trinn inn i det endelige bildet med nginx. Størrelsen på bildet har redusert betydelig:

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

La oss kjøre beholderen med bildet vårt og sørge for at alt fungerer:

docker run -p8080:80 app

Ved å bruke -p8080:80-alternativet videresendte vi port 8080 på vertsmaskinen vår til port 80 inne i beholderen der nginx kjører. Åpne i nettleseren http://localhost:8080/ og vi ser søknaden vår. Alt fungerer!

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Å redusere bildestørrelsen fra 1.74 GB til 36 MB reduserer tiden det tar å levere applikasjonen til produksjon betraktelig. Men la oss gå tilbake til samlingstiden.

$ 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

Endre rekkefølgen på lagene

De tre første trinnene våre ble bufret (hint Using cache). På det fjerde trinnet kopieres alle prosjektfiler og på det femte trinnet installeres avhengigheter RUN npm ci - så mye som 47.338s. Hvorfor installere avhengigheter på nytt hver gang hvis de endres svært sjelden? La oss finne ut hvorfor de ikke ble bufret. Poenget er at Docker vil sjekke lag for lag for å se om kommandoen og filene knyttet til den har endret seg. På det fjerde trinnet kopierer vi alle filene til prosjektet vårt, og blant dem er det selvfølgelig endringer, så Docker tar ikke bare dette laget fra hurtigbufferen, men også alle påfølgende! La oss gjøre noen små endringer i Dockerfilen.

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 .

Først kopieres package.json og package-lock.json, deretter installeres avhengigheter, og først etter det blir hele prosjektet kopiert. Som et resultat:

$ 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 sekunder i stedet for 3 minutter - mye bedre! Riktig rekkefølge av lag er viktig: først kopierer vi det som ikke endres, deretter det som endres sjelden, og til slutt det som endres ofte.

Deretter noen få ord om å sette sammen bilder i CI/CD-systemer.

Bruke tidligere bilder for cache

Hvis vi bruker en slags SaaS-løsning for byggingen, kan den lokale Docker-cachen være ren og frisk. For å gi havnearbeideren et sted å få de bakte lagene, gi ham det forrige bygde bildet.

La oss ta et eksempel på å bygge vår applikasjon i GitHub Actions. Vi bruker denne konfigurasjonen

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

Bildet settes sammen og skyves til GitHub-pakker på to minutter og 20 sekunder:

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

La oss nå endre konstruksjonen slik at en cache brukes basert på tidligere bygde bilder:

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

Først må vi fortelle deg hvorfor to kommandoer startes build. Faktum er at i en flertrinnssammenstilling vil det resulterende bildet være et sett med lag fra det siste stadiet. I dette tilfellet vil ikke lag fra tidligere lag inkluderes i bildet. Derfor, når du bruker det endelige bildet fra en tidligere bygg, vil ikke Docker kunne finne klare lag for å bygge bildet med nodejs (byggerstadiet). For å løse dette problemet opprettes et mellombilde $IMAGE_NAME-builder-stage og skyves til GitHub-pakker slik at den kan brukes i en påfølgende build som en cache-kilde.

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Den totale monteringstiden ble redusert til halvannet minutt. Et halvt minutt brukes på å trekke opp tidligere bilder.

Forhåndsavbildning

En annen måte å løse problemet med en ren Docker-cache på er å flytte noen av lagene til en annen Dockerfil, bygge den separat, skyve den inn i Container Registry og bruke den som en forelder.

Vi lager vårt eget nodejs-bilde for å bygge en Angular-applikasjon. Opprett Dockerfile.node i prosjektet

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

Vi samler inn og publiserer et offentlig bilde i Docker Hub:

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

Nå i vår hoved Dockerfile bruker vi det ferdige bildet:

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

I vårt eksempel ble ikke byggetiden redusert, men forhåndsbygde bilder kan være nyttige hvis du har mange prosjekter og må installere de samme avhengighetene i hvert av dem.

Noen tips om hvordan du kan få fart på byggingen av Docker-bilder. For eksempel opptil 30 sekunder

Vi så på flere metoder for å fremskynde byggingen av docker-bilder. Hvis du vil at distribusjonen skal gå raskt, prøv å bruke dette i prosjektet ditt:

  • redusere kontekst;
  • bruk av små foreldrebilder;
  • flertrinns montering;
  • endre rekkefølgen på instruksjonene i Dockerfilen for å gjøre effektiv bruk av cachen;
  • sette opp en cache i CI/CD-systemer;
  • foreløpig opprettelse av bilder.

Jeg håper eksemplet vil gjøre det klarere hvordan Docker fungerer, og at du vil kunne konfigurere distribusjonen optimalt. For å leke med eksemplene fra artikkelen er det opprettet et depot https://github.com/devopsprodigy/test-docker-build.

Kilde: www.habr.com

Legg til en kommentar