Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Antes de que unha función entre en produción, nestes días de complexos orquestadores e CI/CD, hai un longo camiño por percorrer desde o compromiso ata as probas e a entrega. Anteriormente, podías cargar novos ficheiros a través de FTP (xa ninguén o fai, non?), e o proceso de "implementación" tardaba segundos. Agora cómpre crear unha solicitude de combinación e esperar un tempo considerable para que a función chegue aos usuarios.

Parte deste camiño é construír unha imaxe de Docker. Ás veces a montaxe dura minutos, outras decenas de minutos, o que dificilmente se pode chamar normal. Neste artigo, tomaremos unha aplicación sinxela que empaquetaremos nunha imaxe, aplicaremos varios métodos para acelerar a compilación e analizaremos os matices de como funcionan estes métodos.

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Temos boa experiencia na creación e soporte de sitios web de medios: TASS, A Bell, "Novo xornal", Galicia… Non hai moito que ampliamos a nosa carteira lanzando un sitio web de produtos Recordatorio. E aínda que se engadiron novas funcións rapidamente e se solucionaron erros antigos, a implantación lenta converteuse nun gran problema.

Implementamos a GitLab. Recollemos imaxes, empuxémolas ao rexistro de GitLab e lanzámolas á produción. O máis longo desta lista é montar imaxes. Por exemplo: sen optimización, cada construción do backend levou 14 minutos.

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Ao final, quedou claro que xa non podíamos vivir así, e sentámonos para descubrir por que as imaxes tardaban tanto en recollerse. Como resultado, conseguimos reducir o tempo de montaxe a 30 segundos!

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Para este artigo, para non estar ligado ao entorno de Reminder, vexamos un exemplo de montaxe dunha aplicación Angular baleira. Entón, imos crear a nosa aplicación:

ng n app

Engádelle PWA (somos progresivos):

ng add @angular/pwa --project app

Mentres se están descargando un millón de paquetes npm, imos descubrir como funciona a imaxe docker. Docker ofrece a posibilidade de empaquetar aplicacións e executalas nun ambiente illado chamado contedor. Grazas ao illamento, pode executar moitos contedores á vez nun servidor. Os contedores son moito máis lixeiros que as máquinas virtuais porque se executan directamente no núcleo do sistema. Para executar un contedor coa nosa aplicación, primeiro necesitamos crear unha imaxe na que empaquetaremos todo o necesario para que a nosa aplicación funcione. Esencialmente, unha imaxe é unha copia do sistema de ficheiros. Por exemplo, tome o Dockerfile:

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

Un Dockerfile é un conxunto de instrucións; Ao facer cada un deles, Docker gardará os cambios no sistema de ficheiros e superporáos aos anteriores. Cada equipo crea a súa propia capa. E a imaxe rematada son capas combinadas.

O que é importante saber: cada capa de Docker pode almacenar na caché. Se nada cambiou desde a última compilación, en lugar de executar o comando, o docker tomará unha capa preparada. Dado que o principal aumento da velocidade de compilación será debido ao uso da caché, ao medir a velocidade de compilación prestaremos atención específicamente á construción dunha imaxe cunha caché preparada. Así, paso a paso:

  1. Borramos as imaxes localmente para que as execucións anteriores non afecten á proba.
    docker rmi $(docker images -q)
  2. Lanzamos a construción por primeira vez.
    time docker build -t app .
  3. Cambiamos o ficheiro src/index.html: imitamos o traballo dun programador.
  4. Executamos a construción por segunda vez.
    time docker build -t app .

Se o ambiente para a construción de imaxes está configurado correctamente (máis información a continuación), cando se inicie a compilación, Docker xa terá un montón de cachés a bordo. A nosa tarefa é aprender a usar a caché para que a compilación vaia o máis rápido posible. Dado que asumimos que a execución dunha compilación sen caché só ocorre unha vez, a primeira vez, podemos ignorar o lento que foi esa primeira vez. Nas probas, a segunda execución da compilación é importante para nós, cando os cachés xa están quentados e estamos preparados para facer o noso bolo. Non obstante, algúns consellos tamén afectarán á primeira construción.

Poñamos o Dockerfile descrito anteriormente no cartafol do proxecto e iniciemos a compilación. Todos os listados foron condensados ​​para facilitar a 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

Cambiamos o contido de src/index.html e executámolo unha segunda vez.

$ 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

Para ver se temos a imaxe, executa o comando docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   About a minute ago   1.74GB

Antes de construír, docker toma todos os ficheiros no contexto actual e envíaos ao seu daemon Sending build context to Docker daemon 409MB. O contexto de compilación especifícase como o último argumento do comando de compilación. No noso caso, este é o directorio actual - ".", - e Docker arrastra todo o que temos neste cartafol. 409 MB son moito: pensemos como solucionalo.

Reducindo o contexto

Para reducir o contexto, hai dúas opcións. Ou coloque todos os ficheiros necesarios para a montaxe nun cartafol separado e apunte o contexto docker a este cartafol. Isto pode non ser sempre conveniente, polo que é posible especificar excepcións: o que non se debe arrastrar ao contexto. Para iso, coloque o ficheiro .dockerignore no proxecto e indique o que non é necesario para a compilación:

.git
/node_modules

e executa de novo a compilación:

$ 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 é moito mellor que 409 MB. Tamén reducimos o tamaño da imaxe de 1.74 a 1.38 GB:

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

Tentemos reducir aínda máis o tamaño da imaxe.

Usamos Alpine

Outra forma de aforrar o tamaño da imaxe é usar unha imaxe principal pequena. A imaxe dos pais é a imaxe en base á que se prepara a nosa imaxe. A capa inferior é especificada polo comando FROM en Dockerfile. No noso caso, estamos a usar unha imaxe baseada en Ubuntu que xa ten nodejs instalado. E pesa...

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

... case un gigabyte. Pode reducir significativamente o volume usando unha imaxe baseada en Alpine Linux. Alpine é un Linux moi pequeno. A imaxe docker para nodejs baseada en Alpine só pesa 88.5 MB. Entón, imos substituír a nosa animada imaxe nas casas:

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

Tivemos que instalar algunhas cousas que son necesarias para construír a aplicación. Si, Angular non se constrúe sen Python ¯(°_o)/¯

Pero o tamaño da imaxe baixou a 150 MB:

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

Imos aínda máis alá.

Montaxe multietapa

Non todo o que está na imaxe é o que necesitamos na produción.

$ 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

Con docker run app ls -lah lanzamos un contedor baseado na nosa imaxe app e executou o comando nel ls -lah, despois de que o contedor rematou o seu traballo.

En produción só necesitamos un cartafol dist. Neste caso, os ficheiros dalgún xeito deben darse fóra. Pode executar algún servidor HTTP en nodejs. Pero imos facelo máis doado. Adiviña unha palabra rusa que teña catro letras "y". Certo! Ynzhynyksy. Tomemos unha imaxe con nginx, poñamos nela un cartafol dist e unha pequena configuración:

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

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

A construción en varias etapas axudaranos a facer todo isto. Imos cambiar o noso 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 .

Agora temos dúas instrucións FROM no Dockerfile, cada un deles executa un paso de compilación diferente. Chamamos ao primeiro builder, pero a partir do último FROM, prepararase a nosa imaxe final. O último paso é copiar o artefacto da nosa montaxe na fase anterior na imaxe final con nginx. O tamaño da imaxe diminuíu significativamente:

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

Imos executar o contedor coa nosa imaxe e asegurarnos de que todo funciona:

docker run -p8080:80 app

Usando a opción -p8080:80, reenviamos o porto 8080 da nosa máquina host ao porto 80 dentro do contedor onde se executa nginx. Aberta no navegador http://localhost:8080/ e vemos a nosa aplicación. Todo está funcionando!

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Reducir o tamaño da imaxe de 1.74 GB a 36 MB reduce significativamente o tempo que leva entregar a súa aplicación á produción. Pero volvamos ao tempo da asemblea.

$ 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

Cambiando a orde das capas

Os nosos primeiros tres pasos foron almacenados na caché (suxestión Using cache). No cuarto paso, cópianse todos os ficheiros do proxecto e no quinto paso instálanse as dependencias RUN npm ci - ata 47.338 s. Por que reinstalar as dependencias cada vez se cambian moi poucas veces? Imos descubrir por que non foron almacenados na caché. A cuestión é que Docker comprobará capa por capa para ver se o comando e os ficheiros asociados a el cambiaron. No cuarto paso, copiamos todos os ficheiros do noso proxecto, e entre eles, por suposto, hai cambios, polo que Docker non só non toma esta capa da caché, senón tamén todas as seguintes! Imos facer algúns pequenos cambios no 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 .

En primeiro lugar, cópiase package.json e package-lock.json, despois instálanse as dependencias e só despois cópiase todo o proxecto. Como resultado:

$ 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 segundos en lugar de 3 minutos - moito mellor! A orde correcta das capas é importante: primeiro copiamos o que non cambia, despois o que cambia raramente e, finalmente, o que cambia a miúdo.

A continuación, algunhas palabras sobre a montaxe de imaxes en sistemas CI/CD.

Usando imaxes anteriores para a caché

Se usamos algún tipo de solución SaaS para a compilación, entón a caché local de Docker pode estar limpa e fresca. Para darlle ao docker un lugar onde obter as capas cocidas, dálle a imaxe construída anterior.

Poñamos un exemplo de creación da nosa aplicación en GitHub Actions. Usamos esta configuración

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 imaxe remóntase e envíase aos paquetes de GitHub en dous minutos e 20 segundos:

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Agora imos cambiar a compilación para que se use unha caché baseada nas imaxes construídas anteriores:

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

Primeiro temos que dicirche por que se lanzan dous comandos build. O feito é que nunha montaxe de varias etapas a imaxe resultante será un conxunto de capas da última etapa. Neste caso, as capas das capas anteriores non se incluirán na imaxe. Polo tanto, ao usar a imaxe final dunha compilación anterior, Docker non poderá atopar capas preparadas para construír a imaxe con nodejs (etapa de construción). Para solucionar este problema, créase unha imaxe intermedia $IMAGE_NAME-builder-stage e envíase aos paquetes de GitHub para que se poida usar nunha compilación posterior como fonte de caché.

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

O tempo total de montaxe reduciuse a un minuto e medio. Dedícase medio minuto a sacar imaxes anteriores.

Preimaxe

Outra forma de resolver o problema dunha caché limpa de Docker é mover algunhas das capas a outro Dockerfile, crealo por separado, empúxao no Rexistro de contedores e usalo como pai.

Creamos a nosa propia imaxe nodejs para construír unha aplicación Angular. Crea Dockerfile.node no proxecto

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

Recollemos e enviamos unha imaxe pública en Docker Hub:

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

Agora no noso Dockerfile principal usamos a imaxe rematada:

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

No noso exemplo, o tempo de construción non diminuíu, pero as imaxes preconstruídas poden ser útiles se tes moitos proxectos e tes que instalar as mesmas dependencias en cada un deles.

Algúns consellos sobre como acelerar a creación de imaxes de Docker. Por exemplo, ata 30 segundos

Analizamos varios métodos para acelerar a creación de imaxes docker. Se queres que a implementación vaia rapidamente, proba a usar isto no teu proxecto:

  • reducindo o contexto;
  • uso de pequenas imaxes dos pais;
  • montaxe en varias etapas;
  • cambiar a orde das instrucións no Dockerfile para facer un uso eficiente da caché;
  • configurar unha caché en sistemas CI/CD;
  • creación preliminar de imaxes.

Espero que o exemplo aclare como funciona Docker e poderás configurar de forma óptima a túa implantación. Para xogar cos exemplos do artigo, creouse un repositorio https://github.com/devopsprodigy/test-docker-build.

Fonte: www.habr.com

Engadir un comentario