Než se funkce dostane do produkce, v dnešní době složitých orchestrátorů a CI/CD, je dlouhá cesta od odevzdání k testům a dodání. Dříve jste mohli nahrávat nové soubory přes FTP (to už nikdo nedělá, že?) a proces „nasazení“ trval několik sekund. Nyní je potřeba vytvořit žádost o sloučení a dlouho čekat, než se funkce dostane k uživatelům.
Součástí této cesty je vytvoření image Dockeru. Někdy montáž trvá minuty, někdy desítky minut, což lze jen stěží označit za normální. V tomto článku si vezmeme jednoduchou aplikaci, kterou zabalíme do obrázku, použijeme několik metod pro urychlení sestavení a podíváme se na nuance, jak tyto metody fungují.
Máme dobré zkušenosti s tvorbou a podporou mediálních webů:
Nasazujeme na GitLab. Shromažďujeme obrázky, posíláme je do registru GitLab a vydáváme je do produkce. Nejdelší věcí na tomto seznamu je skládání obrázků. Například: bez optimalizace trvalo každé sestavení backendu 14 minut.
Nakonec bylo jasné, že takhle už dál žít nemůžeme a sedli jsme si, abychom zjistili, proč se snímky sbírají tak dlouho. Díky tomu se nám podařilo zkrátit dobu montáže na 30 sekund!
Pro tento článek, abychom nebyli vázáni na prostředí Reminderu, se podívejme na příklad sestavení prázdné aplikace Angular. Pojďme tedy vytvořit naši aplikaci:
ng n app
Přidejte k tomu PWA (jsme progresivní):
ng add @angular/pwa --project app
Zatímco se stahuje milion balíčků npm, pojďme zjistit, jak funguje obraz dockeru. Docker poskytuje možnost sbalit aplikace a spustit je v izolovaném prostředí zvaném kontejner. Díky izolaci můžete na jednom serveru provozovat více kontejnerů současně. Kontejnery jsou mnohem lehčí než virtuální stroje, protože běží přímo na jádře systému. Pro spuštění kontejneru s naší aplikací si nejprve musíme vytvořit image, do kterého zabalíme vše, co je pro běh naší aplikace potřeba. Obraz je v podstatě kopií systému souborů. Například vezměte Dockerfile:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Dockerfile je sada instrukcí; Provedením každého z nich Docker uloží změny do systému souborů a překryje je na předchozí. Každý tým si vytváří svou vlastní vrstvu. A hotový obrázek jsou vrstvy spojené dohromady.
Co je důležité vědět: každá vrstva Dockeru může ukládat do mezipaměti. Pokud se od posledního sestavení nic nezměnilo, docker místo provedení příkazu vezme hotovou vrstvu. Vzhledem k tomu, že hlavní zvýšení rychlosti sestavení bude způsobeno použitím mezipaměti, při měření rychlosti sestavení budeme věnovat pozornost konkrétně vytváření obrazu s hotovou mezipamětí. Takže krok za krokem:
- Snímky lokálně mažeme, aby předchozí běhy neovlivnily test.
docker rmi $(docker images -q)
- Spouštíme sestavení poprvé.
time docker build -t app .
- Změníme soubor src/index.html - napodobíme práci programátora.
- Spouštíme stavbu podruhé.
time docker build -t app .
Pokud je správně nakonfigurováno prostředí pro vytváření obrazů (více o tom níže), pak při spuštění sestavování bude mít Docker již na palubě spoustu mezipamětí. Naším úkolem je naučit se používat cache tak, aby stavba probíhala co nejrychleji. Vzhledem k tomu, že předpokládáme, že ke spuštění sestavení bez mezipaměti dojde pouze jednou – úplně poprvé – můžeme tedy ignorovat, jak pomalé to poprvé bylo. V testech je pro nás důležité druhé spuštění sestavení, kdy jsou kešky již rozehřáté a my jsme připraveni upéct náš dort. Některé tipy však ovlivní i první sestavení.
Vložme Dockerfile popsaný výše do složky projektu a spusťte sestavování. Všechny výpisy byly zhuštěné pro snadnější čtení.
$ 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
Změníme obsah souboru src/index.html a spustíme jej podruhé.
$ 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
Chcete-li zjistit, zda máme obrázek, spusťte příkaz docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
Před sestavením docker vezme všechny soubory v aktuálním kontextu a odešle je svému démonovi Sending build context to Docker daemon 409MB
. Kontext sestavení je zadán jako poslední argument příkazu sestavení. V našem případě je to aktuální adresář - ".", - a Docker přetáhne vše, co v této složce máme. 409 MB je hodně: zamysleme se nad tím, jak to opravit.
Omezení kontextu
Chcete-li omezit kontext, existují dvě možnosti. Nebo umístěte všechny soubory potřebné pro sestavení do samostatné složky a nasměrujte kontext dockeru na tuto složku. To nemusí být vždy vhodné, takže je možné specifikovat výjimky: co by se nemělo zatahovat do kontextu. Chcete-li to provést, vložte soubor .dockerignore do projektu a uveďte, co není pro sestavení potřeba:
.git
/node_modules
a znovu spusťte sestavení:
$ 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 je mnohem lepší než 409 MB. Také jsme snížili velikost obrázku z 1.74 na 1.38 GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
Zkusme ještě zmenšit velikost obrázku.
Používáme Alpine
Dalším způsobem, jak ušetřit na velikosti obrázku, je použít malý rodičovský obrázek. Rodičovský obrázek je obrázek, na jehož základě je připraven náš obrázek. Spodní vrstva je určena příkazem FROM
v Dockerfile. V našem případě používáme obraz založený na Ubuntu, který již má nainstalovaný nodejs. A váží...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... téměř gigabajt. Hlasitost můžete výrazně snížit použitím obrazu založeného na Alpine Linuxu. Alpine je velmi malý Linux. Docker image pro nodejs založený na alpine váží pouze 88.5 MB. Pojďme tedy nahradit náš živý obraz v domech:
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 nutné k sestavení aplikace. Ano, Angular se nesestaví bez Pythonu ¯(°_o)/¯
Ale velikost obrázku klesla na 150 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
Pojďme ještě dále.
Vícestupňová montáž
Ne vše, co je na obrázku, je to, co potřebujeme ve výrobě.
$ 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
S docker run app ls -lah
spustili jsme kontejner založený na našem obrázku app
a provedl v něm příkaz ls -lah
, načež kontejner dokončil svou práci.
Při výrobě potřebujeme pouze složku dist
. V tomto případě je třeba soubory nějak dát ven. Na nodejs můžete spustit nějaký HTTP server. Ale my si to usnadníme. Hádejte ruské slovo, které má čtyři písmena „y“. Že jo! Ynzhynyksy. Udělejme obrázek s nginx, dáme do něj složku dist
a malá konfigurace:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
K tomu všemu nám pomůže vícestupňové sestavení. Pojďme změnit 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 .
Nyní máme dva pokyny FROM
v Dockerfile každý z nich spustí jiný krok sestavení. Zavolali jsme prvnímu builder
, ale počínaje posledním FROM bude připraven náš konečný obrázek. Posledním krokem je zkopírování artefaktu naší sestavy v předchozím kroku do výsledného obrázku pomocí nginx. Velikost obrázku se výrazně zmenšila:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
Spusťte kontejner s naším obrázkem a ujistěte se, že vše funguje:
docker run -p8080:80 app
Pomocí volby -p8080:80 jsme předali port 8080 na našem hostitelském počítači na port 80 uvnitř kontejneru, kde běží nginx. Otevřít v prohlížeči
Zmenšením velikosti obrazu z 1.74 GB na 36 MB se výrazně zkrátí čas potřebný k dodání vaší aplikace do produkce. Ale vraťme se k době montáže.
$ 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
Změna pořadí vrstev
Naše první tři kroky byly uloženy do mezipaměti (nápověda Using cache
). Ve čtvrtém kroku se zkopírují všechny soubory projektu a v pátém kroku se nainstalují závislosti RUN npm ci
- až 47.338 s. Proč pokaždé znovu instalovat závislosti, když se mění velmi zřídka? Pojďme zjistit, proč nebyly uloženy do mezipaměti. Jde o to, že Docker bude vrstvu po vrstvě kontrolovat, zda se příkaz a soubory s ním spojené změnily. Ve čtvrtém kroku zkopírujeme všechny soubory našeho projektu a mezi nimi jsou samozřejmě změny, takže Docker nejen že nebere tuto vrstvu z mezipaměti, ale ani všechny následující! Udělejme nějaké malé změny 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 teprve poté se zkopíruje celý projekt. Jako 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 – mnohem lepší! Důležité je správné pořadí vrstev: nejprve zkopírujeme to, co se nemění, pak to, co se mění zřídka, a nakonec to, co se často mění.
Dále pár slov o sestavování obrazů v systémech CI/CD.
Použití předchozích obrázků pro vyrovnávací paměť
Pokud pro sestavení použijeme nějaký druh řešení SaaS, pak může být místní mezipaměť Dockeru čistá a čerstvá. Chcete-li dockerovi poskytnout místo, kde může získat pečené vrstvy, dejte mu předchozí vytvořený obrázek.
Vezměme si příklad vytváření naší aplikace v GitHub Actions. Používáme tuto 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
Obrázek je sestaven a odeslán do balíčků GitHub za dvě minuty a 20 sekund:
Nyní změňme sestavení tak, aby se mezipaměť používala na základě předchozích vytvořených obrázků:
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 vám musíme říci, proč jsou spuštěny dva příkazy build
. Faktem je, že ve vícestupňové sestavě bude výsledným obrazem sada vrstev z poslední fáze. V tomto případě nebudou do obrázku zahrnuty vrstvy z předchozích vrstev. Proto při použití konečného obrazu z předchozího sestavení Docker nebude schopen najít připravené vrstvy pro sestavení obrazu pomocí nodejs (fáze tvůrce). Za účelem vyřešení tohoto problému je vytvořen meziobraz $IMAGE_NAME-builder-stage
a je odeslán do balíčků GitHub, aby mohl být použit v následném sestavení jako zdroj mezipaměti.
Celková doba montáže se zkrátila na jednu a půl minuty. Půl minuty strávíte stahováním předchozích obrázků.
Preimaging
Dalším způsobem, jak vyřešit problém čisté mezipaměti Dockeru, je přesunout některé vrstvy do jiného souboru Dockerfile, sestavit jej samostatně, vložit jej do registru kontejnerů a použít jej jako nadřazený soubor.
Vytváříme vlastní image nodejs pro vytvoření aplikace Angular. 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++
Shromažďujeme a posíláme veřejný obrázek v 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žíváme hotový obrázek:
FROM exsmund/node-for-angular:latest as builder
...
V našem příkladu se doba sestavení nezkrátila, ale předpřipravené obrazy mohou být užitečné, pokud máte mnoho projektů a musíte do každého z nich nainstalovat stejné závislosti.
Podívali jsme se na několik metod, jak urychlit vytváření obrázků dockeru. Pokud chcete, aby nasazení proběhlo rychle, zkuste ve svém projektu použít toto:
- redukce kontextu;
- použití malých rodičovských obrázků;
- vícestupňová montáž;
- změna pořadí instrukcí v Dockerfile za účelem efektivního využití mezipaměti;
- nastavení mezipaměti v systémech CI/CD;
- předběžná tvorba obrázků.
Doufám, že příklad objasní, jak Docker funguje, a budete moci optimálně nakonfigurovat své nasazení. Abychom si mohli pohrát s příklady z článku, bylo vytvořeno úložiště
Zdroj: www.habr.com