Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Před tím, než se funkce dostane na produkci, je třeba projít dlouhou cestou od commitu přes testy až k nasazení v dnešní době složitých orchestrátorů a CI/CD. Dříve stačilo nahrát nové soubory pomocí FTP (to už dnes nikdo nedělá, že?), a proces „deploy“ trval sekundy. Dnes je nutné vytvořit merge request a čekat, než se funkce dostane k uživatelům.

Část z této cesty je build Docker obrazu. Někdy build trvá minuty, jindy desítky minut, což se těžko dá považovat za normální. V tomto článku vezmeme jednoduchou aplikaci, zabalíme ji do obrazu, použijeme několik metod pro zrychlení buildu a prozkoumáme nuance těchto metod.

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Máme slušné zkušenosti s vytvářením a údržbou webů pro média: TASS, The Bell, "Novaja gazeta", Republic… Nedávno jsme rozšířili naše portfolio tím, že jsme spustili web Reminder. A zatímco jsme rychle dodělávali nové funkce a opravovali staré chyby, pomalý deploy se stal velkým problémem.

Deploy provádíme na GitLab. Vytváříme obrazy, pushujeme do GitLab Registry a nasazujeme na produkci. Nejdelší částí tohoto procesu je právě build obrazů. Například: bez optimalizace každý build backendu trval 14 minut.

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Nakonec se ukázalo, že takto žít nelze, a usadili jsme se, abychom zjistili, proč trvá build obrazů tak dlouho. Nakonec se nám podařilo zkrátit dobu buildu na 30 sekund!

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Pro tento článek, abychom se nevázali k prostředí Reminderu, prozkoumáme příklad buildu prázdné aplikace na Angular. Takže, vytvořme naši aplikaci:

ng n app

Přidáme do ní PWA (jsme přece progresivní):

ng add @angular/pwa --project app

Zatímco se stahuje milion npm balíčků, podívejme se, jak funguje docker obraz. Docker umožňuje balit aplikace a spouštět je v izolovaném prostředí, které se nazývá kontejner. Díky izolaci můžeme spouštět více kontejnerů na jednom serveru. Kontajnery jsou mnohem lehčí než virtuální stroje, protože běží přímo na jádru systému. Abychom spustili kontejner s naší aplikací, musíme nejdříve vytvořit obraz, do kterého zabalíme vše, co je potřeba pro běh naší aplikace. V podstatě je obraz otiskem souborového systému. Například, vezmeme Dockerfile:

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

Dockerfile je sada pokynů; při vykonávání každého z nich Docker uloží změny do souborového systému a aplikuje je na předchozí. Každý příkaz vytváří svou vlastní vrstvu. Hotový obraz je pak spojením těchto vrstev.

Co je důležité vědět: každá vrstva může být cachována. Pokud se nic nezměnilo od poslední kompilace, Docker místo vykonávání příkazu vezme již hotovou vrstvu. Hlavní přínos v rychlosti kompilace bude díky využívání cachí, a proto se při měření rychlosti kompilace budeme soustředit právě na kompilaci obrazu s již připravenou cache. Takže postupně:

  1. Odstraňujeme obrazy lokálně, aby předchozí spuštění neovlivnila test.
    docker rmi $(docker images -q)
  2. Spouštíme build poprvé.
    time docker build -t app .
  3. Měníme soubor src/index.html – imitujeme práci programátora.
  4. Spouštíme build podruhé.
    time docker build -t app .

Pokud je prostředí pro budování obrazů správně nakonfigurováno (o čemž bude řeč níže), Docker při spuštění build bude mít k dispozici spoustu cachí. Naším úkolem je naučit se využívat cache tak, aby kompilace probíhala co nejrychleji. Předpokládáme, že spuštění build bez cache probíhá jen jednou – poprvé – tedy můžeme ignorovat, jak pomalé bylo toto první spuštění. V testech je pro nás důležitý druhý běh kompilace, kdy jsou cache již aktivovány a jsme připraveni péct náš koláč. Nicméně některé rady se projeví už při prvním buildování.

Uložíme výše popsaný Dockerfile do složky s projektem a spustíme build. Všechny uvedené úryvky byly zkráceny pro snazší čtení.

$ time docker build -t app .
Odesílání kontextu build do Docker démona 409MB
Krok 1/5 : FROM node:12.16.2
Stav: Stáhnuta novější verze obrazu pro node:12.16.2
Krok 2/5 : WORKDIR /app
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37581ms
Úspěšně vytvořeno c8c279335f46
Úspěšně označeno app:latest

skutečný 5m4.541s
uživatelský 0m0.000s
systémový 0m0.000s

Měníme obsah src/index.html a spouštíme podruhé.

$ time docker build -t app .
Odesílání kontextu build do Docker démona 409MB
Krok 1/5 : FROM node:12.16.2
Krok 2/5 : WORKDIR /app
 ---> Používá cache
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37902ms
Úspěšně vytvořeno 79f335df92d3
Úspěšně označeno app:latest

skutečný 3m33.262s
uživatelský 0m0.000s
systémový 0m0.000s

Abychom zjistili, jestli jsme úspěšně vytvořili obraz, provedeme příkaz docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   Před asi minutou   1.74GB

Před sestavením Docker shromáždí všechny soubory v aktuálním kontextu a odešle je svému démonu. Odesílání kontextu sestavení do démonu Docker 409MB. Kontext pro sestavení se uvádí jako poslední argument příkazu build. V našem případě je to aktuální adresář – „.“, – a Docker přenáší všechno, co máme v této složce. 409 MB je hodně: přemýšlejme, jak to napravit.

Snižujeme kontext

Abychom snížili kontext, existují dvě možnosti. Buď přesunout všechny soubory potřebné pro sestavení do samostatné složky a poskytnout kontext Dockeru právě na tuto složku. To nemusí být vždy pohodlné, a proto máme možnost uvést výjimky: co nemusíme přenášet do kontextu. K tomu přidáme do projektu soubor .dockerignore a uvedeme, co není potřeba pro sestavení:

.git
/node_modules

a spustíme sestavení znovu:

$ time docker build -t app .
Odesílání kontextu sestavení do démonu Docker 607.2kB
Krok 1/5 : Z FROM node:12.16.2
Krok 2/5 : WORKDIR /app
 ---> Použití vyrovnávací paměti
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37313ms
Úspěšně sestaveno 4942f010792a
Úspěšně označeno app:latest

reálné 1m47.763s
uživatel 0m0.000s
systém 0m0.000s

607.2 Kb — mnohem lepší než 409 MB. A ještě jsme zmenšili velikost obrazu z 1.74 na 1.38GB:

REPOSITÁŘ   TAG      ID OBRAZU       VYTVOŘENO         VELIKOST
app          latest   4942f010792a   před 3 minutami   1.38GB

Zkusme obraz zmenšit ještě více.

Používáme Alpine

Dalším způsobem, jak ušetřit na velikosti obrazu, je použít malý základní obraz. Základní obraz je obraz, na jehož základě je náš obraz připravován. Spodní vrstvu uvádíme příkazem FROM v Dockerfile. V našem případě používáme obraz založený na Ubuntu, který již má nainstalovaný nodejs. A jeho váha je …

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c před 17 minutami 916MB

… téměř gigabyte. Výrazně snížit objem lze použitím obrazu založeného na Alpine Linux. Alpine je velmi malý linux. Docker obraz pro nodejs na základě Alpine váží pouze 88.5 MB. Takže pojďme nahradit náš tlustý základní obraz:

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

Museli jsme nainstalovat některé věci, které jsou potřebné pro sestavení aplikace. Ano, Angular se nesestaví bez Pythona ¯(°_o)¯

Ale velikost obrazu klesla o 150 MB:

REPOSITÁŘ   TAG      ID OBRAZU       VYTVOŘENO          VELIKOST
app          latest   aa031edc315a   před 22 minutami   761MB

Jdeme dál.

Více fází sestavení

Ne všechno, co je v obraze, potřebujeme v produkci.

$ 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

Pomocí docker run app ls -lah Spustili jsme kontejner na základě našeho obrazu app a vykonali v něm příkaz ls -lah, po kterém kontejner ukončil svou činnost.

Na produkci potřebujeme pouze složku dist. Přitom je potřeba nějakým způsobem sdílet soubory ven. Můžeme spustit nějaký HTTP server na nodejs. Ale uděláme to jednoduše. Uhodněte ruské slovo, které má čtyři písmena „ы“. Správně! Ынжыныксы. Vezmeme obraz s nginx, vložíme do něj složku dist a malý konfigurák:

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

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

To vše nám pomůže provést multi-stage build. Změníme 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 . .
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 .

Teď máme dvě instrukce FROM v Dockerfile, každá z nich spouští svůj fázi sestavení. První jsme nazvali builder, ale od posledního FROM se bude připravovat náš finální obraz. Posledním krokem je zkopírovat artefakt našeho sestavení z předchozí fáze do finálního obrazu s nginx. Velikost obrazu se významně zmenšila:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   2c6c5da07802   před 29 minutami   36MB

Pojďme spustit kontejner s naším obrazem a ujistěme se, že vše funguje:

docker run -p8080:80 app

S parametrem -p8080:80 jsme propojili port 8080 na našem hostitelském stroji s portem 80 uvnitř kontejneru, kde běží nginx. Otevřeme v prohlížeči http://localhost:8080/ a vidíme naši aplikaci. Vše funguje!

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Snížení velikosti obrazu z 1,74 GB na 36 MB výrazně zkracuje dobu dodání vaší aplikace do produkce. Ale vraťme se k času sestavení.

$ 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

Měníme pořadí vrstev

První tři kroky byly vyrovnávány (nápověda Using cache). V čtvrtém kroku se kopírují všechny soubory projektu a v pátém kroku se instalují závislosti RUN npm ci — celých 47,338 s. Proč pokaždé znovu instalovat závislosti, když se mění jen velmi zřídka? Pojďme si objasnit, proč se neuložily do cache. Je to tím, že Docker kontroluje vrstvu po vrstvě, zda se nezměnily příkazy a související soubory. Ve čtvrtém kroku kopírujeme všechny soubory našeho projektu a samozřejmě mezi nimi jsou změny, takže Docker nejenže nebere tuto vrstvu z cache, ale ani všechny následující! Pojďme provést malé úpravy v 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 .

Nejprve se zkopírují package.json a package-lock.json, poté se nainstalují závislosti a až nakonec se zkopíruje celý projekt. Výsledek:

$ 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 sekund místo 3 minut — výrazně lepší! Důležitý je správný pořádek vrstev: nejprve kopírujeme to, co se nemění, poté to, co se mění zřídka, a nakonec to, co se mění často.

Dále několik slov o sestavení obrazů v CI/CD systémech.

Použití předchozích obrazů pro cache

Pokud používáme k sestavení nějaké SaaS řešení, může být lokální cache Dockeru čistá a čerstvá. Aby měl Docker odkud vzít upečené vrstvy, dejte mu předchozí sestavený obraz.

Podívejme se na sestavení naší aplikace v GitHub Actions. Použijeme takovou konfiguraci.

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

Obraz je shromážděn a nahrán do GitHub Packages za dvě minuty a 20 sekund:

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Nyní změníme sestavení tak, aby používalo mezipaměť založenou na předchozích shromážděných obrazech:

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

Nejprve je třeba vysvětlit, proč se spouští dvě příkazy build. To je proto, že v multi-stage sestavení bude výsledným obrazem sada vrstev z poslední fáze. Přitom vrstvy z předchozích vrstev nebyly zahrnuty do obrazu. Proto, když se používá konečný obraz z předchozího sestavení, Docker nebude moci najít připravené vrstvy pro sestavení obrazu s nodejs (fáze builder). Abychom tento problém vyřešili, vytvoříme meziprochodný obraz $IMAGE_NAME-builder-stage a odešleme ho do GitHub Packages, aby ho bylo možné použít v následném sestavení jako zdroj mezipaměti.

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Celkový čas sestavení se zkrátil na jednu a půl minuty. Půl minuty se ztrácí při stahování předchozích obrazů.

Předběžné vytvoření obrazů

Další způsob, jak vyřešit problém s čistou mezipamětí Dockeru, je část vrstev vyčlenit do jiného Dockerfile, sestavit ho samostatně, nahrát do Container Registry a použít jako rodičovský.

Vytváříme svůj obraz nodejs pro sestavení Angular aplikace. V projektu vytvořte Dockerfile.node

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

Sestavujeme a nahráváme veřejný obraz do Docker Hub:

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

Nyní v našem hlavním Dockerfile použijeme hotový obraz:

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

V našem příkladu se doba sestavení nezkrátila, ale předem vytvořené obrazy mohou být užitečné, pokud máte mnoho projektů a v každém z nich musíte instalovat stejné závislosti.

Několik tipů, jak zrychlit build Docker obrazů. Například na 30 sekund.

Prozkoumali jsme několik metod zrychlení sestavení docker obrazů. Pokud chcete, aby nasazení probíhalo rychle, zkuste ve svém projektu použít:

  • snížení kontextu;
  • používání malých rodičovských obrazů;
  • multi-stage build;
  • změnu pořadí instrukcí v Dockerfile, aby bylo možné efektivně využít mezipaměť;
  • nastavení mezipaměti v CI/CD systémech;
  • předběžné vytváření obrazů.

Doufám, že na příkladu bude jasnější, jak Docker funguje, a budete moci optimálně nastavit své nasazení. Abychom si mohli vyzkoušet příklady z článku, byl vytvořen repozitář https://github.com/devopsprodigy/test-docker-build.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster