Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Înainte ca o funcționalitate să ajungă în producție, în vremea orchestratorilor complexi și a CI/CD-ului, trebuie să parcurgă un drum lung de la commit la teste și livrare. În trecut, puteai să arunci fișierele noi prin FTP (dar acum nimeni nu mai face așa, nu-i așa?), iar procesul de „deploiere” dura doar câteva secunde. Acum, trebuie să creezi o cerere de fuziune și să aștepți un timp considerabil până când funcționalitatea ajunge la utilizatori.

O parte din acest drum este construirea imaginii Docker. Uneori construirea durează minute, alteori zeci de minute, ceea ce nici măcar nu poate fi numit normal. În acest articol, vom lua o aplicație simplă, pe care o vom împacheta într-o imagine, vom aplica câteva metode pentru a accelera construcția și vom analiza detaliile acestor metode.

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Avem o experiență bună în crearea și întreținerea site-urilor media: TASS, The Bell, "Gazeta Nouă", Republic… Nu cu mult timp în urmă, ne-am completat portofoliul, lansând site-ul în producție Reminder. Și în timp ce lucram rapid la noi funcționalități și reparam erorile vechi, deploierea lentă a devenit o mare problemă.

Deploierea o facem pe GitLab. Construim imagini, le împingem în GitLab Registry și le desfășurăm în producție. În această listă, cea mai lungă etapă este construirea imaginilor. De exemplu: fără optimizare, fiecare construcție a backend-ului dura 14 minute.

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

În cele din urmă, a devenit clar că nu mai putem continua așa, așa că ne-am așezat să înțelegem de ce imaginile se construiesc atât de mult timp. În final, am reușit să reducem timpul de construcție la 30 de secunde!

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Pentru acest articol, pentru a nu ne lega de mediu Reminder, vom analiza un exemplu de construcție a unei aplicații goale pe Angular. Așadar, să creăm aplicația noastră:

ng n app

Adăugăm PWA (suntem progresiști):

ng add @angular/pwa --project app

În timp ce se descarcă milioane de pachete npm, să ne dăm seama cum este construită imaginea docker. Docker oferă posibilitatea de a împacheta aplicații și a le rula într-un mediu izolat, numit container. Datorită izolării, poți rula simultan multe containere pe un singur server. Containerele sunt mult mai ușoare decât mașinile virtuale, deoarece rulează direct pe nucleul sistemului. Pentru a rula un container cu aplicația noastră, trebuie mai întâi să creăm o imagine în care vom împacheta tot ce este necesar pentru funcționarea aplicației noastre. Practic, imaginea este o copie a sistemului de fișiere. De exemplu, să luăm un Dockerfile:

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

Dockerfile este un set de instrucțiuni; executând fiecare dintre ele, Docker va salva modificările în sistemul de fișiere și le va suprapune pe cele anterioare. Fiecare comandă creează propriul său strat. Iar imaginea finală este o combinație a acestor straturi.

Ce este important de știut: fiecare strat poate fi stocat în cache de Docker. Dacă nu s-a schimbat nimic de la ultima construcție, în loc să execute comanda, Docker va folosi deja stratul finalizat. Deoarece câștigul principal în viteza de construcție va veni din utilizarea cache-ului, vom acorda o atenție deosebită măsurării vitezei construcției atunci când imaginile sunt construite cu cache. Așadar, iată pașii:

  1. Ștergem imaginile local pentru ca execuțiile anterioare să nu influențeze testul.
    docker rmi $(docker images -q)
  2. Executăm construcția pentru prima dată.
    time docker build -t app .
  3. Schimbăm fișierul src/index.html — imităm munca programatorului.
  4. Executăm construcția pentru a doua oară.
    time docker build -t app .

Dacă mediul pentru construcția imaginilor este configurat corect (despre care vom discuta puțin mai jos), atunci Docker va avea deja o mulțime de cache-uri disponibile la începutul construcției. Sarcina noastră este să învățăm să folosim cache-ul astfel încât construcția să fie cât mai rapidă. Deoarece presupunem că execuția construcției fără cache se întâmplă doar o singură dată — prima — putem ignora cât de lentă a fost acea primă dată. În teste, contează mai mult a doua execuție a construcției, când cache-urile sunt deja preîncălzite și suntem gata să ne coacem plăcinta. Cu toate acestea, unele sfaturi vor influența și prima construcție.

Să plasăm Dockerfile-ul, descris mai sus, în folderul proiectului și să inițiem construcția. Toate listările prezentate sunt reduse pentru a facilita citirea.

$ 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

Modificăm conținutul src/index.html și inițiem din nou construcția.

$ 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

Pentru a verifica dacă am obținut imaginea, vom executa comanda docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   Aproape acum un minut   1.74GB

Înainte de a construi, Docker ia toate fișierele din contextul actual și le trimite demonului său. Trimiterea contextului de construcție către demonul Docker 409MB. Contextul pentru construcție este specificat ca ultimul argument al comenzii build. În cazul nostru, acesta este directorul curent — „.” — și Docker aduce tot ce avem în această dosar. 409 MB este mult: să ne gândim cum putem corecta asta.

Reducerea contextului

Pentru a reduce contextul, există două opțiuni. Fie să punem toate fișierele necesare pentru construcție într-un dosar separat și să indicăm Docker-ului exact acest dosar ca context. Aceasta poate fi uneori incomod, așa că există opțiunea de a specifica excepții: ce nu trebuie să fie inclus în context. Pentru aceasta, vom adăuga un fișier .dockerignore în proiect și vom specifica ce nu este necesar pentru construcție:

.git
/node_modules

și vom rula construcția din nou:

$ time docker build -t app .
Trimiterea contextului de construcție către demonul Docker 607.2kB
Pasul 1/5 : DIN node:12.16.2
Pasul 2/5 : WORKDIR /app
 ---> Folosind cache
Pasul 3/5 : COPIE . .
Pasul 4/5 : RULARE npm ci
adăugat 1357 pachete în 22.47s
Pasul 5/5 : RULARE npm run build --prod
Data: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Timp: 37313ms
Construit cu succes 4942f010792a
Etichetat cu succes app:latest

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

607.2 Kбайт — mult mai bine decât 409 MB. Și am redus dimensiunea imaginii de la 1.74 la 1.38 GB:

REPOZITAR   ETICHETĂ   ID IMAGINE       CREAT         DIMENSIUNE
app          latest   4942f010792a   acum 3 minute   1.38GB

Hai să încercăm să reducem și mai mult dimensiunea imaginii.

Folosim Alpine

Înca o metodă de a economisi pe dimensiunea imaginii — utilizarea unei imagini parentale mai mici. Imaginea parentală este imaginea pe care se bazează imaginea noastră. Stratul de bază este specificat cu comanda FROM în Dockerfile. În cazul nostru, folosim o imagine bazată pe Ubuntu, care are deja nodejs instalat. Și cântărește …

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c acum 17 minute 916MB

… aproape un gigabyte. Se poate reduce semnificativ volumul, folosind o imagine bazată pe Alpine Linux. Alpine este un Linux foarte mic. Imaginea Docker pentru nodejs bazată pe alpine cântărește doar 88.5 MB. Așadar, hai să înlocuim imaginea noastră prea grea:

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

A trebuit să instalăm câteva lucruri necesare pentru construirea aplicației. Da, Angular nu se compilează fără Python ¯(°_o)\/¯

Dar dimensiunea imaginii a scăzut cu 150 MB:

REPOZITAR   ETICHETĂ   ID IMAGINE       CREAT          DIMENSIUNE
app          latest   aa031edc315a   acum 22 minute   761MB

Să mergem mai departe.

Construire multi-stage

Nu tot ce există în imagine ne este necesar în producție.

$ 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

Folosind docker run app ls -lah Am lansat un container bazat pe imaginea noastră app și am executat comanda în el ls -lah, după care containerul și-a terminat activitatea.

Pe producție, avem nevoie doar de folderul dist. Totuși, trebuie să expunem fișierele. Putem rula un server HTTP folosind nodejs. Dar vom face mai simplu. Ghiciți cuvântul românesc care are patru litere „î”. Corect! Îndjînii. Vom lua o imagine cu nginx, vom plasa în ea folderul dist și un mic fișier de configurare:

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

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

Acest lucru ne va ajuta să utilizăm un multi-stage build. Vom modifica Dockerfile-ul nostru:

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 .

Acum avem două instrucțiuni FROM în Dockerfile, fiecare dintre ele rulează propria etapă de construcție. Prima a fost numită builder, iar de la ultimul FROM se va pregăti imaginea noastră finală. Pasul final este să copiem artefactul construcției noastre din etapa anterioară în imaginea finală cu nginx. Dimensiunea imaginii a fost semnificativ redusă:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   2c6c5da07802   acum 29 de minute   36MB

Hai să lansăm un container cu imaginea noastră și să ne asigurăm că totul funcționează:

docker run -p8080:80 app

Cu opțiunea -p8080:80 am redirecționat portul 8080 de pe mașina noastră gazdă către portul 80 din interiorul containerului, unde rulează nginx. Deschidem în browser http://localhost:8080/ și vedem aplicația noastră. Totul funcționează!

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Reducerea dimensiunii imaginii de la 1.74 GB la 36 MB scurtează semnificativ timpul necesar pentru livrarea aplicației tale în producție. Dar să ne întoarcem la timpul de construcție.

$ time docker build -t app .
Trimite contextul construcției daemon-ului Docker 608.8kB
Pasul 1/11 : FROM node:12.16.2-alpine3.11 as builder
Pasul 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
 ---> Using cache
Pasul 3/11 : WORKDIR /app
 ---> Using cache
Pasul 4/11 : COPY . .
Pasul 5/11 : RUN npm ci
au fost adăugate 1357 pachete în 47.338s
Pasul 6/11 : RUN npm run build --prod
Data: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Timp: 39948ms
 ---> 27f1479221e4
Pasul 7/11 : FROM nginx:stable-alpine
Pasul 8/11 : WORKDIR /app
 ---> Using cache
Pasul 9/11 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Using cache
Pasul 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
 ---> Using cache
Pasul 11/11 : COPY --from=builder /app/dist/app .
Construcție reușită d201471c91ad
Etichetă reușită app:latest

real 2m17.700s
user 0m0.000s
sys 0m0.000s

Schimbăm ordinea straturilor

Primele trei pași au fost cache-uite (sugestie Using cache). La pasul al patrulea se copiază toate fișierele proiectului, iar la pasul al cincilea se instalează dependențele RUN npm ci — întregi 47,338s. De ce să instalăm dependențele de fiecare dată, dacă ele se schimbă foarte rar? Hai să vedem de ce nu au fost stocate în cache. Problema este că Docker verifică strat cu strat, dacă comanda și fișierele asociate cu aceasta s-au schimbat. În pasul patru copiem toate fișierele proiectului nostru, iar printre ele, desigur, există modificări, așa că Docker nu numai că nu ia din cache acest strat, dar și toate cele ulterioare! Să facem câteva modificări în 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 .

Mai întâi sunt copiate package.json și package-lock.json, apoi se instalează dependențele, și abia după aceea se copiază întreg proiectul. Ca rezultat:

$ 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 secunde în loc de 3 minute — mult mai bine! Este important să avem ordinea corectă a straturilor: mai întâi copiem ceea ce nu se schimbă, apoi ceea ce se schimbă rar, iar la final — ceea ce se schimbă frecvent.

Apoi, câteva cuvinte despre construirea imaginilor în sistemele CI\/CD.

Utilizarea imaginilor anterioare pentru cache

Dacă folosim o soluție SaaS pentru construirea aplicației, cache-ul local Docker poate fi gol și proaspăt. Pentru ca Docker să aibă de unde să ia straturile coapte, oferă-i imaginea construită anterior.

Să luăm ca exemplu construirea aplicației noastre în GitHub Actions. Folosim această configurație

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

Imaginea este construită și împinsă în GitHub Packages în două minute și 20 de secunde:

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Acum vom modifica construcția pentru a folosi cache-ul bazat pe imaginile construite anterior:

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

În primul rând, trebuie să explicăm de ce sunt rulate două comenzi. buildCeea ce se întâmplă este că, în construcția multi-stagiu, rezultatul va fi un set de straturi din ultimul stagiu. Straturile din straturile anterioare nu vor fi incluse în imagine. Prin urmare, când se folosește imaginea finală din construcția anterioară, Docker nu va putea găsi straturile necesare pentru a construi imaginea cu nodejs (stagiu builder). Pentru a rezolva această problemă, se creează o imagine intermediară. $IMAGE_NAME-builder-stage și este trimisă în GitHub Packages, astfel încât să poată fi folosită în construcțiile ulterioare ca sursă de cache.

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Timpul total de construcție a fost redus la o minută și jumătate. O jumătate de minut este consumată pentru a atrage imaginile anterioare.

Crearea preliminară a imaginilor

O altă modalitate de a rezolva problema memoriei cache în Docker este să extindem o parte din straturi într-un alt Dockerfile, să-l compilăm separat, să-l încărcăm în Container Registry și să-l folosim ca părinte.

Creăm imaginea noastră nodejs pentru compilarea aplicației Angular. Creăm un Dockerfile.node în proiect.

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

Construim și încărcăm imaginea publică în Docker Hub:

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

Acum, în Dockerfile-ul nostru principal, folosim imaginea gata făcută:

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

În exemplul nostru, timpul de construire nu s-a redus, dar imaginile pre-construite pot fi utile dacă aveți multe proiecte și în fiecare dintre ele trebuie să instalați aceleași dependențe.

Câteva sfaturi despre cum să accelerăm construirea imaginilor Docker. De exemplu, până la 30 de secunde

Am analizat câteva metode de accelerare a construirii imaginilor Docker. Dacă doriți ca desfășurarea să fie rapidă, încercați să aplicați în proiectul dumneavoastră:

  • reducerea contextului;
  • utilizarea unor imagini părinte mici;
  • construcția cu multiple stadii;
  • schimbarea ordinii instrucțiunilor în Dockerfile pentru a utiliza eficient memoria cache;
  • configurarea cache-ului în sistemele CI/CD;
  • crearea prealabilă a imaginilor.

Sper că exemplul va face mai clar cum funcționează Docker și că veți putea să vă optimizați desfășurarea. Pentru a experimenta cu exemplele din articol, s-a creat un depozit https://github.com/devopsprodigy/test-docker-build.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster