Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Mielőtt egy új funkció élesbe kerül, a bonyolult orkestrátorok és CI/CD világában hosszú utat kell megtennie a pillanattól a tesztelésig és a kiszállításig. Régen elég volt az új fájlokat FTP-n feltölteni (de most már senki sem csinálja így, igaz?), és a "telepítési" folyamat másodpercekbe telt. Most azonban merge kérdést kell készíteni, és eltart egy ideig, amíg a funkció eljut a felhasználókhoz.

Ennek az útnak egy része a Docker-image-ek összeállítása. Néha az összeállítás percekig, néha pedig hosszú percekig tart, ami nehezen mondható normálisnak. Ebben a cikkben egy egyszerű alkalmazást fogunk csomagolni egy image-be, alkalmazunk néhány módszert az összeállítás felgyorsítására, és megvizsgáljuk e módszerek működésének részleteit.

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Jó tapasztalataink vannak a hírszolgáltatások weboldalainak létrehozásában és támogatásában: TASS, The Bell, "Új Újság", Republic… Nemrégiben új weboldallal bővítettük a portfóliónkat, amely az élesben futó Reminder. Miközben gyorsan kidolgoztuk az új funkciókat és javítottuk a régi hibákat, a lassú telepítés nagy problémává vált.

A telepítést GitLab-on végezzük. Összeállítjuk az image-eket, feltöltjük a GitLab Registry-be, majd élesben telepítjük őket. Ebben a folyamatban a legidőigényesebb a image-ek összeállítása. Például optimalizálás nélkül minden backend összeállítás 14 percig tartott.

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Végül világossá vált, hogy így nem lehet tovább élni, és nekiláttunk kideríteni, miért tart ennyi ideig az image-ek összeállítása. Végül sikerült az összeállítási időt 30 másodpercre csökkenteni!

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Ehhez a cikkhez, hogy ne kössünk a Reminder környezetéhez, egy üres Angular alkalmazás összeállításának példáját fogjuk megvizsgálni. Tehát létrehozzuk az alkalmazásunkat:

ng n app

Hozzáadjuk a PWA-t (hiszen haladók vagyunk):

ng add @angular/pwa --project app

Miközben letöltődik a rengeteg npm-csomag, nézzük meg, hogyan néz ki a docker-image. A Docker lehetővé teszi, hogy alkalmazásokat csomagoljunk és egy izolált környezetben, úgynevezett konténerben futtassuk őket. Az izolációnak köszönhetően egyszerre több konténer is futhat egy szerveren. A konténerek lényegesen könnyebbek, mint a virtuális gépek, mivel közvetlenül a rendszer magjában futnak. Ahhoz, hogy elindítsuk a konténert az alkalmazásunkkal, először létre kell hoznunk egy image-et, amelybe mindent csomagolunk, amire az alkalmazásunk működéséhez szükség van. Az image lényegében a fájlrendszer lenyomata. Például nézzük a Dockerfile-t:

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

A Dockerfile egy utasításkészlet; amikor ezeket végrehajtja, a Docker a fájlrendszeren tárolja a módosításokat, és rájuk rakja az előző rétegeket. Minden parancs létrehozza a saját rétegét. A kész kép a rétegek összegyúrása.

Fontos tudni: minden réteget a Docker képes gyorsítani. Ha semmi nem változott az előző build óta, a Docker a korábban elkészült réteget veszi figyelembe a parancs végrehajtása helyett. Mivel a sebesség növekedése a cache használatából származik, a build sebességének mérésekor csak a cache-elt képek építésére figyelünk. Tehát, lépésről lépésre:

  1. Töröljük a helyi képeket, hogy az előző futások ne befolyásolják a tesztet.
    docker rmi $(docker images -q)
  2. Indítsuk el a buildet először.
    time docker build -t app .
  3. Módosítjuk a fájlt src/index.html — szimulálva egy programozó munkáját.
  4. Indítjuk a buildet másodszor.
    time docker build -t app .

Ha a képek buildeléséhez szükséges környezetet megfelelően konfiguráltuk (erről kicsit később), a Docker a build elindításakor már sok cache-t fog használni. Célunk, hogy megtanuljuk a cache-t használni, hogy a build a lehető leggyorsabban menjen végbe. Feltételezve, hogy a cache nélküli buildet csak egyszer, az első alkalommal hajtjuk végre, azt figyelmen kívül hagyhatjuk, mennyire lassú volt az első alkalom. A tesztjeink szempontjából a második build fontos, amikor a cache-ek már felmelegedtek, és készen állunk a sütemény sütésére. Mindazonáltal néhány tipp az első buildre is hatással lesz.

Tegyük a fent leírt Dockerfile-t a projekt mappájába, és indítsuk el a buildet. Minden bemutatott részlet a könnyebb olvashatóság érdekében lett rövidítve.

$ time docker build -t app .
Küldés a build kontextusát a Docker démonnak 409MB
1/5. lépés: FROM node:12.16.2
Állapot: Újabb image letöltve a node:12.16.2-hez
2/5. lépés: WORKDIR /app
3/5. lépés: COPY . .
4/5. lépés: RUN npm ci
1357 csomag hozzáadva 22.47s alatt
5/5. lépés: RUN npm run build --prod
Dátum: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Idő: 37581ms
Sikeresen építve c8c279335f46
Sikeresen címkézve app:latest

valós 5m4.541s
felhasználó 0m0.000s
rendszer 0m0.000s

Módosítjuk a src/index.html tartalmát és indítjuk másodszor.

$ time docker build -t app .
Küldés a build kontextusát a Docker démonnak 409MB
1/5. lépés: FROM node:12.16.2
2/5. lépés: WORKDIR /app
 ---> Cache használata
3/5. lépés: COPY . .
4/5. lépés: RUN npm ci
1357 csomag hozzáadva 22.47s alatt
5/5. lépés: RUN npm run build --prod
Dátum: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Idő: 37902ms
Sikeresen építve 79f335df92d3
Sikeresen címkézve app:latest

valós 3m33.262s
felhasználó 0m0.000s
rendszer 0m0.000s

Ahhoz, hogy megnézzük, elkészült-e a kép, futtassuk a parancsot docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   Körülbelül egy perce   1.74GB

A Docker build takes all files in the current context and sends them to its daemon. Sending build context to Docker daemon 409MB. The build context is specified as the last argument of the build command. In our case, this is the current directory — "." — and Docker pulls everything we have in this folder. 409 MB is a lot: let's think about how to fix this.

Reducing the context

To reduce the context, there are two options. Either place all the files needed for the build in a separate folder and specify that folder as the context for Docker. This may not always be convenient, so there's a way to specify exceptions: what should not be included in the context. For this, we will add a .dockerignore file to the project and specify what is not needed for the build:

.git
/node_modules

and run the build again:

$ 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 is much better than 409 MB. And we also reduced the image size from 1.74 to 1.38 GB:

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

Let's try to further reduce the image size.

Using Alpine

Another way to save on image size is to use a smaller parent image. The parent image is the image on which our image is based. The lower layer is specified by the command FROM in the Dockerfile. In our case, we are using an image based on Ubuntu that already has nodejs installed. And it weighs ...

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

... almost a gigabyte. You can significantly reduce the size by using an image based on Alpine Linux. Alpine is a very small Linux. The Docker image for nodejs based on Alpine weighs only 88.5 MB. So let's replace our fat parent image:

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

We had to install some stuff that is necessary for building the application. Yes, Angular doesn't build without Python ¯(°_o)\/¯

But the image size dropped by 150 MB:

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

Let's go further.

Multi-stage build

Not everything that is in the image is needed in production.

$ 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

A segítségével docker run app ls -lah Elindítottunk egy konténert a saját képmásunk alapján. app és végrehajtottuk benne a parancsot ls -lah, ezt követően a konténer befejezte a működését.

A produkcióhoz csak a mappára van szükségünk dist. A fájlokat valahogy ki kell adni. Indíthatunk egy HTTP-szervert node.js-en. De egyszerűsítünk. Találjátok ki a négy 'ы' betűt tartalmazó orosz szót. Helyes! Ынжыныксы. Válasszuk ki az nginx képet, tegyük bele a mappát dist és egy egyszerű konfigurációt:

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

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

Ezt a multi-stage build segít nekünk megvalósítani. Módosítjuk a Dockerfile-t:

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 .

Most két utasításunk van FROM a Dockerfile-ban, mindegyik saját építési szakaszát indítja el. Az elsőt neveztük Torrentek:, az utolsó FROM-tól kezdve készül a végső képmásunk. Az utolsó lépésben átmásoljuk az előző szakaszban készített artefaktumot a végső nginx képmásba. A képmás mérete jelentősen csökkent:

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

Indítsuk el a konténert a képmásunkkal, és győződjünk meg róla, hogy minden működik:

docker run -p8080:80 app

A -p8080:80 opcióval a 8080-as portot az otthoni gépünkön a konténer 80-as portjára irányítottuk, ahol az nginx fut. Nyissuk meg a böngészőben http://localhost:8080/ és láthatjuk az alkalmazásunkat. Minden működik!

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

A képmás méretének csökkentése 1.74 GB-ról 36 MB-ra jelentősen lerövidíti az alkalmazásunk szállítási idejét a produkcióba. De térjünk vissza az építési időre.

$ 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

Megváltoztatjuk a rétegek sorrendjét

Az első három lépés már cache-elt (tipp: Using cache). A negyedik lépésben a projekt összes fájlát átmásolják, az ötödik lépésben a függőségek telepítése történik RUN npm ci — összesen 47,338 másodperc. Miért kellene minden alkalommal újra telepíteni a függőségeket, ha azok nagyon ritkán változnak? Nézzük meg, miért nem kerültek eltárolásra a gyorsítótárban. Az a helyzet, hogy a Docker rétegről rétegre ellenőrzi, hogy megváltozott-e a parancs és a hozzá kapcsolódó fájlok. A negyedik lépésben átmásoljuk a projektünk összes fájlját, és ezek között természetesen vannak változások, így a Docker nemcsak hogy nem veszi az ezt a réteget a gyorsítótárból, hanem az összes következőt sem! Tekintsük át, hogyan módosíthatnánk a Dockerfile-t.

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 .

Először a package.json és a package-lock.json fájlok másolódnak, majd telepítjük a függőségeket, és csak ezután másoljuk az egész projektet. Az eredmény:

$ time docker build -t app .
A build kontextusának küldése a Docker démonnak 608,8 kB
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
Dátum: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Idő: 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 .
Sikeresen létrehozva: a44dd7c217c3
Sikeresen címkézve: app:latest

valódi 0m46.497s
user 0m0.000s
sys 0m0.000s

46 másodperc a 3 perc helyett — sokkal jobb! Fontos a rétegek megfelelő sorrendje: először másoljuk azt, ami nem változik, majd azt, ami ritkán változik, végül pedig azt, ami gyakran.

Ezután néhány szó a képek CI/CD rendszerekben történő létrehozásáról.

Előző képek használata gyorsítótárként

Ha valamilyen SaaS megoldást használunk a buildhez, akkor a helyi Docker gyorsítótár üres és friss lehet. Ahhoz, hogy a Dockernek legyen honnan elvenni a feltöltött rétegeket, adjunk neki egy korábban elkészült képet.

Nézzük meg például az alkalmazásunk GitHub Actions-ban történő buildelését. Használjunk egy ilyen konfigurációt

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

A képet két perc és 20 másodperc alatt építjük és toljuk a GitHub Packages-be:

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Most módosítsuk az építést, hogy a korábban elkészült képek alapján cache-t használjunk:

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

Először is el kell mondani, miért indítunk el két parancsot. buildAzért, mert a multi-stage build során az eredmény egy rétegcsomag lesz az utolsó stagből. Ezzel együtt az előző rétegek nem kerülnek az image-be. Ezért, ha a végső képet a korábbi buildből használjuk, a Docker nem találja meg a kész rétegeket a Node.js image építéséhez (builder stage). E probléma megoldására egy közbenső image-t hozunk létre. $IMAGE_NAME-builder-stage és feltöltjük a GitHub Packages-be, hogy a következő buildnél cache forrásként felhasználhassuk.

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Az összes építési idő egy és fél percre csökkent. Fél perc alatt az előző képek lehívására van szükség.

Előzetes képkészítés

Egy másik módja a Docker tiszta gyorsítótárának problémájának megoldására az, hogy néhány réteget áthelyezünk egy másik Dockerfile-ba, külön összeállítjuk, feltöltjük a Container Registry-be, és szülőként használjuk.

Saját nodejs képet készítünk az Angular-alkalmazás összeállításához. Létrehozzuk a projektben a Dockerfile.node fájlt.

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

Készítünk és feltöltünk nyilvános képet a Docker Hub-ra:

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

Most a fő Dockerfile-unkban használjuk a kész képet:

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

A példánkban a build idő nem csökkent, de az előre elkészített képek hasznosak lehetnek, ha sok projekted van, és mindegyikben azonos függőségeket kell telepíteni.

Néhány tipp a Docker-képfájlok gyorsabb létrehozásához. Például akár 30 másodperc alatt

Több módszert is megvizsgáltunk a Docker-képek buildelésének felgyorsítására. Ha azt szeretnéd, hogy a telepítés gyors legyen, próbáld alkalmazni a projektedben:

  • a kontextus csökkentése;
  • kisméretű szülőképek használata;
  • multistage build;
  • az utasítások sorrendjének megváltoztatása a Dockerfile-ban a gyorsítótár hatékony felhasználásához;
  • a gyorsítótár beállítása CI/CD rendszerekben;
  • előre elkészített képek.

Remélem, hogy a példa segít megérteni, hogyan működik a Docker, és hogy optimálisan be tudod állítani a telepítésedet. A cikk példáinak kipróbálásához készült egy repository. https://github.com/devopsprodigy/test-docker-build.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster