Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Antes de que una función entre en producción, en estos días de orquestadores complejos y CI/CD, hay un largo camino por recorrer desde el compromiso hasta las pruebas y la entrega. Anteriormente, podías cargar archivos nuevos a través de FTP (ya nadie hace eso, ¿verdad?) y el proceso de “implementación” tomaba unos segundos. Ahora debe crear una solicitud de combinación y esperar mucho tiempo hasta que la función llegue a los usuarios.

Parte de este camino es crear una imagen de Docker. A veces la asamblea dura unos minutos, a veces decenas de minutos, lo que difícilmente puede considerarse normal. En este artículo, tomaremos una aplicación simple que empaquetaremos en una imagen, aplicaremos varios métodos para acelerar la compilación y veremos los matices de cómo funcionan estos métodos.

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Tenemos buena experiencia en la creación y soporte de sitios web de medios: TASS, La Campana, Novaya Gazeta, República… No hace mucho ampliamos nuestra cartera con el lanzamiento de un sitio web de productos. recordatorio. Y aunque rápidamente se agregaron nuevas funciones y se corrigieron errores antiguos, la lentitud en la implementación se convirtió en un gran problema.

Implementamos en GitLab. Recopilamos imágenes, las enviamos al Registro GitLab y las implementamos en producción. Lo más largo de esta lista es ensamblar imágenes. Por ejemplo: sin optimización, cada compilación de backend tomó 14 minutos.

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Al final, quedó claro que ya no podíamos vivir así y nos sentamos a descubrir por qué tardaban tanto en recopilarse las imágenes. ¡Como resultado, logramos reducir el tiempo de montaje a 30 segundos!

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Para este artículo, para no estar vinculados al entorno de Reminder, veamos un ejemplo de cómo ensamblar una aplicación Angular vacía. Entonces, creemos nuestra aplicación:

ng n app

Agregue PWA (somos progresistas):

ng add @angular/pwa --project app

Mientras se descargan un millón de paquetes npm, descubramos cómo funciona la imagen de la ventana acoplable. Docker brinda la capacidad de empaquetar aplicaciones y ejecutarlas en un entorno aislado llamado contenedor. Gracias al aislamiento, puedes ejecutar muchos contenedores simultáneamente en un servidor. Los contenedores son mucho más livianos que las máquinas virtuales porque se ejecutan directamente en el núcleo del sistema. Para ejecutar un contenedor con nuestra aplicación, primero necesitamos crear una imagen en la que empaquetaremos todo lo necesario para que se ejecute nuestra aplicación. Básicamente, una imagen es una copia del sistema de archivos. Por ejemplo, tome el Dockerfile:

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

Un Dockerfile es un conjunto de instrucciones; Al realizar cada uno de ellos, Docker guardará los cambios en el sistema de archivos y los superpondrá a los anteriores. Cada equipo crea su propia capa. Y la imagen terminada son capas combinadas.

Lo que es importante saber: cada capa de Docker puede almacenarse en caché. Si nada ha cambiado desde la última compilación, en lugar de ejecutar el comando, la ventana acoplable tomará una capa ya preparada. Dado que el principal aumento en la velocidad de compilación se deberá al uso del caché, al medir la velocidad de compilación prestaremos atención específicamente a la creación de una imagen con un caché ya preparado. Entonces, paso a paso:

  1. Eliminamos las imágenes localmente para que las ejecuciones anteriores no afecten la prueba.
    docker rmi $(docker images -q)
  2. Lanzamos la compilación por primera vez.
    time docker build -t app .
  3. Cambiamos el archivo src/index.html: imitamos el trabajo de un programador.
  4. Ejecutamos la compilación por segunda vez.
    time docker build -t app .

Si el entorno para crear imágenes está configurado correctamente (más sobre esto a continuación), cuando comience la compilación, Docker ya tendrá un montón de cachés a bordo. Nuestra tarea es aprender a utilizar el caché para que la compilación se realice lo más rápido posible. Dado que asumimos que ejecutar una compilación sin caché solo ocurre una vez (la primera vez), podemos ignorar cuán lenta fue esa primera vez. En las pruebas, la segunda ejecución de la compilación es importante para nosotros, cuando los cachés ya están calentados y estamos listos para hornear nuestro pastel. Sin embargo, algunos consejos también afectarán a la primera construcción.

Coloquemos el Dockerfile descrito anteriormente en la carpeta del proyecto y comencemos la compilación. Todos los listados se han condensado para 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

Cambiamos el contenido de src/index.html y lo ejecutamos por 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 si tenemos la imagen ejecutamos el comando docker images:

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

Antes de compilar, Docker toma todos los archivos en el contexto actual y los envía a su demonio. Sending build context to Docker daemon 409MB. El contexto de compilación se especifica como último argumento del comando de compilación. En nuestro caso, este es el directorio actual - ".", - y Docker arrastra todo lo que tenemos en esta carpeta. 409 MB es mucho: pensemos en cómo solucionarlo.

Reducir el contexto

Para reducir el contexto, hay dos opciones. O coloque todos los archivos necesarios para el ensamblaje en una carpeta separada y apunte el contexto de la ventana acoplable a esta carpeta. Puede que esto no siempre sea conveniente, por lo que es posible especificar excepciones: lo que no debe arrastrarse al contexto. Para hacer esto, coloque el archivo .dockerignore en el proyecto e indique lo que no es necesario para la compilación:

.git
/node_modules

y ejecuta la compilación nuevamente:

$ 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 es mucho mejor que 409 MB. También redujimos el tamaño de la imagen de 1.74 a 1.38 GB:

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

Intentemos reducir aún más el tamaño de la imagen.

Usamos alpino

Otra forma de ahorrar en el tamaño de la imagen es utilizar una imagen principal pequeña. La imagen parental es la imagen a partir de la cual se prepara nuestra imagen. La capa inferior está especificada por el comando. FROM en Dockerfile. En nuestro caso, estamos usando una imagen basada en Ubuntu que ya tiene nodejs instalado. Y pesa...

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

... casi un gigabyte. Puede reducir significativamente el volumen utilizando una imagen basada en Alpine Linux. Alpine es un Linux muy pequeño. La imagen de la ventana acoplable para nodejs basada en alpine pesa solo 88.5 MB. Así que reemplacemos nuestra imagen animada en las 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

Tuvimos que instalar algunas cosas necesarias para crear la aplicación. Sí, Angular no se construye sin Python ¯(°_o)/¯

Pero el tamaño de la imagen se redujo a 150 MB:

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

Vayamos aún más lejos.

Montaje multietapa

No todo lo que está en la imagen es lo que necesitamos en producció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 contenedor basado en nuestra imagen app y ejecuté el comando en él ls -lah, tras lo cual el contenedor completó su trabajo.

En producción solo necesitamos una carpeta. dist. En este caso, los archivos deben entregarse de alguna manera al exterior. Puede ejecutar algún servidor HTTP en nodejs. Pero lo haremos más fácil. Adivina una palabra rusa que tenga cuatro letras “y”. ¡Bien! Ynzhynyksy. Tomemos una imagen con nginx y coloquemos una carpeta en ella. dist y una pequeña configuración:

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

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

La construcción en varias etapas nos ayudará a hacer todo esto. Cambiemos nuestro 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 .

Ahora tenemos dos instrucciones. FROM en Dockerfile, cada uno de ellos ejecuta un paso de compilación diferente. Llamamos al primero. builder, pero a partir del último DESDE, se preparará nuestra imagen final. El último paso es copiar el artefacto de nuestro ensamblaje en el paso anterior a la imagen final con nginx. El tamaño de la imagen ha disminuido significativamente:

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

Ejecutemos el contenedor con nuestra imagen y asegurémonos de que todo funcione:

docker run -p8080:80 app

Usando la opción -p8080:80, reenviamos el puerto 8080 en nuestra máquina host al puerto 80 dentro del contenedor donde se ejecuta nginx. Abierta en el navegador http://localhost:8080/ y vemos nuestra aplicación. ¡Todo está funcionando!

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Reducir el tamaño de la imagen de 1.74 GB a 36 MB reduce significativamente el tiempo que lleva entregar su aplicación a producción. Pero volvamos al tiempo de montaje.

$ 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

Cambiar el orden de las capas

Nuestros primeros tres pasos se almacenaron en caché (pista Using cache). En el cuarto paso, se copian todos los archivos del proyecto y en el quinto paso se instalan las dependencias. RUN npm ci - hasta 47.338s. ¿Por qué reinstalar dependencias cada vez si cambian muy raramente? Averigüemos por qué no se almacenaron en caché. El punto es que Docker comprobará capa por capa para ver si el comando y los archivos asociados a él han cambiado. En el cuarto paso, copiamos todos los archivos de nuestro proyecto, y entre ellos, por supuesto, hay cambios, por lo que Docker no solo no toma esta capa del caché, ¡sino también todas las siguientes! Hagamos algunos pequeños cambios en 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 .

Primero, se copian package.json y package-lock.json, luego se instalan las dependencias y solo después se copia todo el proyecto. 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, ¡mucho mejor! El orden correcto de las capas es importante: primero copiamos lo que no cambia, luego lo que cambia raramente y finalmente lo que cambia con frecuencia.

A continuación, algunas palabras sobre el ensamblaje de imágenes en sistemas CI/CD.

Usando imágenes anteriores para caché

Si utilizamos algún tipo de solución SaaS para la compilación, entonces la caché local de Docker puede estar limpia y actualizada. Para darle al acoplador un lugar para obtener las capas horneadas, proporciónele la imagen creada anteriormente.

Tomemos un ejemplo de cómo crear nuestra 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

La imagen se ensambla y se envía a GitHub Packages en dos minutos y 20 segundos:

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Ahora cambiemos la compilación para que se use un caché basado en imágenes compiladas 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

Primero debemos decirle por qué se lanzan dos comandos. build. El hecho es que en un montaje de varias etapas la imagen resultante será un conjunto de capas de la última etapa. En este caso, las capas de capas anteriores no se incluirán en la imagen. Por lo tanto, cuando se utiliza la imagen final de una compilación anterior, Docker no podrá encontrar capas listas para construir la imagen con nodejs (etapa de construcción). Para solucionar este problema se crea una imagen intermedia. $IMAGE_NAME-builder-stage y se envía a paquetes de GitHub para que pueda usarse en una compilación posterior como fuente de caché.

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

El tiempo total de montaje se redujo a un minuto y medio. Se dedica medio minuto a recuperar imágenes anteriores.

Preimagen

Otra forma de resolver el problema de un caché Docker limpio es mover algunas de las capas a otro Dockerfile, compilarlo por separado, insertarlo en el Container Registry y usarlo como padre.

Creamos nuestra propia imagen de nodejs para construir una aplicación Angular. Cree Dockerfile.node en el proyecto

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

Recopilamos y publicamos una imagen pública en Docker Hub:

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

Ahora en nuestro Dockerfile principal usamos la imagen terminada:

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

En nuestro ejemplo, el tiempo de compilación no disminuyó, pero las imágenes prediseñadas pueden ser útiles si tienes muchos proyectos y tienes que instalar las mismas dependencias en cada uno de ellos.

Algunos consejos sobre cómo acelerar la creación de imágenes de Docker. Por ejemplo, hasta 30 segundos.

Analizamos varios métodos para acelerar la creación de imágenes acoplables. Si desea que la implementación se realice rápidamente, intente usar esto en su proyecto:

  • reducir el contexto;
  • uso de imágenes parentales pequeñas;
  • montaje de varias etapas;
  • cambiar el orden de las instrucciones en Dockerfile para hacer un uso eficiente del caché;
  • configurar un caché en sistemas CI/CD;
  • creación preliminar de imágenes.

Espero que el ejemplo aclare cómo funciona Docker y pueda configurar de manera óptima su implementación. Para jugar con los ejemplos del artículo se ha creado un repositorio https://github.com/devopsprodigy/test-docker-build.

Fuente: habr.com

Añadir un comentario