Abans que una funció entri en producció, en aquests dies d'orquestradors complexos i CI/CD, hi ha un llarg camí per recórrer des del compromís fins a les proves i el lliurament. Anteriorment, podríeu pujar fitxers nous mitjançant FTP (ja no ho fa ningú, oi?), i el procés de "desplegament" va trigar segons. Ara heu de crear una sol·licitud de combinació i esperar molt de temps perquè la funció arribi als usuaris.
Part d'aquest camí és crear una imatge de Docker. De vegades el muntatge dura minuts, de vegades desenes de minuts, cosa que difícilment es pot dir normal. En aquest article, agafarem una aplicació senzilla que empaquetarem en una imatge, aplicarem diversos mètodes per accelerar la compilació i veurem els matisos de com funcionen aquests mètodes.
Tenim una bona experiència en la creació i suport de llocs web de mitjans:
Despleguem a GitLab. Recopilem imatges, les enviem a GitLab Registry i les distribuïm a la producció. El més llarg d'aquesta llista és muntar imatges. Per exemple: sense optimització, cada construcció de backend va trigar 14 minuts.
Al final, va quedar clar que ja no podíem viure així, i ens vam asseure per esbrinar per què les imatges tardaven tant a recollir-se. Com a resultat, vam aconseguir reduir el temps de muntatge a 30 segons!
Per a aquest article, per no estar lligat a l'entorn de Reminder, mirem un exemple de muntatge d'una aplicació Angular buida. Per tant, creem la nostra aplicació:
ng n app
Afegiu-hi PWA (som progressistes):
ng add @angular/pwa --project app
Mentre s'estan descarregant un milió de paquets npm, anem a esbrinar com funciona la imatge de Docker. Docker ofereix la possibilitat d'empaquetar aplicacions i executar-les en un entorn aïllat anomenat contenidor. Gràcies a l'aïllament, podeu executar molts contenidors simultàniament en un servidor. Els contenidors són molt més lleugers que les màquines virtuals perquè s'executen directament al nucli del sistema. Per executar un contenidor amb la nostra aplicació, primer hem de crear una imatge en la qual empaquetarem tot el que sigui necessari perquè la nostra aplicació s'executi. Bàsicament, una imatge és una còpia del sistema de fitxers. Per exemple, preneu el Dockerfile:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Un Dockerfile és un conjunt d'instruccions; En fer cadascun d'ells, Docker desarà els canvis al sistema de fitxers i els superposarà als anteriors. Cada equip crea la seva pròpia capa. I la imatge acabada són capes combinades.
El que és important saber: cada capa de Docker es pot emmagatzemar a la memòria cau. Si no ha canviat res des de l'última compilació, en lloc d'executar l'ordre, l'acoblador agafarà una capa ja feta. Atès que l'augment principal de la velocitat de construcció es deu a l'ús de la memòria cau, quan mesurem la velocitat de construcció prestarem atenció específicament a la creació d'una imatge amb una memòria cau ja feta. Així doncs, pas a pas:
- Esborram les imatges localment perquè les execucions anteriors no afectin la prova.
docker rmi $(docker images -q)
- Llencem la construcció per primera vegada.
time docker build -t app .
- Canviem el fitxer src/index.html: imitem el treball d'un programador.
- Executem la construcció per segona vegada.
time docker build -t app .
Si l'entorn per a la creació d'imatges està configurat correctament (més informació a continuació), quan s'iniciï la compilació, Docker ja tindrà un munt de memòria cau a bord. La nostra tasca és aprendre a utilitzar la memòria cau perquè la compilació vagi el més ràpid possible. Com que suposem que l'execució d'una compilació sense memòria cau només es fa una vegada, la primera vegada, podem ignorar la lentitud que va ser aquesta primera vegada. A les proves, la segona execució de la construcció és important per a nosaltres, quan els cachés ja estan escalfats i estem preparats per coure el nostre pastís. Tanmateix, alguns consells també afectaran la primera construcció.
Posem el Dockerfile descrit anteriorment a la carpeta del projecte i comencem la compilació. Tots els llistats s'han condensat per facilitar la lectura.
$ 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
Canviem el contingut de src/index.html i l'executem una segona vegada.
$ 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
Per veure si tenim la imatge, executeu l'ordre docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
Abans de construir, Docker agafa tots els fitxers en el context actual i els envia al seu dimoni Sending build context to Docker daemon 409MB
. El context de compilació s'especifica com l'últim argument de l'ordre de compilació. En el nostre cas, aquest és el directori actual - ".", - i Docker arrossega tot el que tenim en aquesta carpeta. 409 MB són molts: pensem com solucionar-ho.
Reduint el context
Per reduir el context, hi ha dues opcions. O col·loqueu tots els fitxers necessaris per al muntatge en una carpeta separada i assenyaleu el context del docker a aquesta carpeta. Això pot no ser sempre convenient, de manera que és possible especificar excepcions: què no s'ha d'arrossegar al context. Per fer-ho, poseu el fitxer .dockerignore al projecte i indiqueu què no és necessari per a la compilació:
.git
/node_modules
i torna a executar la construcció:
$ 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 és molt millor que 409 MB. També hem reduït la mida de la imatge d'1.74 a 1.38 GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
Intentem reduir encara més la mida de la imatge.
Utilitzem Alpine
Una altra manera d'estalviar la mida de la imatge és utilitzar una imatge principal petita. La imatge parental és la imatge a partir de la qual es prepara la nostra imatge. La capa inferior s'especifica mitjançant l'ordre FROM
a Dockerfile. En el nostre cas, estem utilitzant una imatge basada en Ubuntu que ja té instal·lat nodejs. I pesa...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... gairebé un gigabyte. Podeu reduir significativament el volum utilitzant una imatge basada en Alpine Linux. Alpine és un Linux molt petit. La imatge docker per a nodejs basada en alpine només pesa 88.5 MB. Així que substituïm la nostra imatge animada a les cases:
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
Hem hagut d'instal·lar algunes coses que són necessàries per construir l'aplicació. Sí, Angular no es construeix sense Python ¯(°_o)/¯
Però la mida de la imatge va baixar a 150 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
Anem encara més enllà.
Muntatge multietapa
No tot el que hi ha a la imatge és el que necessitem a la producció.
$ 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
Amb docker run app ls -lah
vam llançar un contenidor basat en la nostra imatge app
i hi va executar l'ordre ls -lah
, després del qual el contenidor va completar la seva feina.
En producció només necessitem una carpeta dist
. En aquest cas, els fitxers s'han de donar d'alguna manera fora. Podeu executar algun servidor HTTP a nodejs. Però ho posarem més fàcil. Endevina una paraula russa que tingui quatre lletres "y". Dret! Ynzhynyksy. Prenguem una imatge amb nginx, posem-hi una carpeta dist
i una petita configuració:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
La construcció en diverses etapes ens ajudarà a fer tot això. Canviem el nostre 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 .
Ara tenim dues instruccions FROM
al Dockerfile, cadascun d'ells executa un pas de construcció diferent. Vam trucar al primer builder
, però a partir de l'últim FROM, es prepararà la nostra imatge final. L'últim pas és copiar l'artefacte del nostre conjunt del pas anterior a la imatge final amb nginx. La mida de la imatge ha disminuït significativament:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
Executem el contenidor amb la nostra imatge i assegurem-nos que tot funciona:
docker run -p8080:80 app
Utilitzant l'opció -p8080:80, vam reenviar el port 8080 de la nostra màquina amfitrió al port 80 dins del contenidor on s'executa nginx. Oberta al navegador
La reducció de la mida de la imatge d'1.74 GB a 36 MB redueix significativament el temps que triga a lliurar la vostra aplicació a producció. Però tornem al temps del muntatge.
$ 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
Canvi de l'ordre de les capes
Els nostres primers tres passos es van guardar a la memòria cau (suggerència Using cache
). Al quart pas, es copien tots els fitxers del projecte i al cinquè pas s'instal·len les dependències RUN npm ci
- fins a 47.338 s. Per què tornar a instal·lar les dependències cada cop si canvien molt poques vegades? Anem a esbrinar per què no es van guardar a la memòria cau. La qüestió és que Docker comprovarà capa per capa per veure si l'ordre i els fitxers associats amb ella han canviat. En el quart pas, copiem tots els fitxers del nostre projecte, i entre ells, per descomptat, hi ha canvis, de manera que Docker no només no treu aquesta capa de la memòria cau, sinó també totes les posteriors! Anem a fer alguns petits canvis al 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 .
Primer, es copien package.json i package-lock.json, després s'instal·len les dependències i només després es copia tot el projecte. Com a 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 segons en lloc de 3 minuts, molt millor! L'ordre correcte de les capes és important: primer copiem allò que no canvia, després allò que canvia poques vegades i, finalment, allò que canvia sovint.
A continuació, algunes paraules sobre el muntatge d'imatges en sistemes CI/CD.
Ús d'imatges anteriors per a la memòria cau
Si utilitzem algun tipus de solució SaaS per a la compilació, la memòria cau local de Docker pot estar neta i fresca. Per donar-li al docker un lloc per obtenir les capes al forn, doneu-li la imatge construïda anterior.
Prenguem un exemple de creació de la nostra aplicació a GitHub Actions. Utilitzem aquesta configuració
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
La imatge es munta i s'envia als paquets GitHub en dos minuts i 20 segons:
Ara canviem la compilació de manera que s'utilitzi una memòria cau basada en imatges construïdes anteriors:
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
Primer hem d'explicar-vos per què s'executen dues ordres build
. El fet és que en un muntatge multietapa la imatge resultant serà un conjunt de capes de l'última etapa. En aquest cas, les capes de les capes anteriors no s'inclouran a la imatge. Per tant, quan utilitzeu la imatge final d'una compilació anterior, Docker no podrà trobar capes preparades per construir la imatge amb nodejs (etapa de constructor). Per solucionar aquest problema, es crea una imatge intermèdia $IMAGE_NAME-builder-stage
i s'envia als paquets GitHub perquè es pugui utilitzar en una compilació posterior com a font de memòria cau.
El temps total de muntatge es va reduir a un minut i mig. Es dediquen mig minut a treure imatges anteriors.
Preimatge
Una altra manera de resoldre el problema d'una memòria cau de Docker neta és moure algunes de les capes a un altre Dockerfile, crear-lo per separat, introduir-lo al Registre de contenidors i utilitzar-lo com a pare.
Creem la nostra pròpia imatge de nodejs per construir una aplicació Angular. Creeu Dockerfile.node al projecte
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
Recopilem i emeten una imatge pública a Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest
Ara al nostre Dockerfile principal fem servir la imatge acabada:
FROM exsmund/node-for-angular:latest as builder
...
En el nostre exemple, el temps de construcció no va disminuir, però les imatges preconstruïdes poden ser útils si teniu molts projectes i heu d'instal·lar les mateixes dependències en cadascun d'ells.
Hem analitzat diversos mètodes per accelerar la creació d'imatges docker. Si voleu que el desplegament vagi ràpidament, proveu d'utilitzar això al vostre projecte:
- reducció del context;
- ús de petites imatges dels pares;
- muntatge en diverses etapes;
- canviar l'ordre de les instruccions al Dockerfile per fer un ús eficient de la memòria cau;
- configurar una memòria cau en sistemes CI/CD;
- creació preliminar d'imatges.
Espero que l'exemple deixi més clar com funciona Docker i podreu configurar de manera òptima el vostre desplegament. Per jugar amb els exemples de l'article, s'ha creat un repositori
Font: www.habr.com