Acelerando los desarrollos con contenedores: Optimizando imágenes Docker con Dive

Publicado por Víctor Madrid el

Arquitectura de SolucionesDockerDive

En este artículo se va a mostrar como poder disponer de una herramienta que pueda ayudar a analizar una imagen Docker, a entender la forma en la que se ha construido (capas, operaciones, configuración, etc.) y sobre todo a ayudar a optimizarla (tamaño, orden adecuado de las operaciones, mejoras en los tiempos de construcción, etc.)

A veces NO nos damos cuenta del tiempo que perdemos y la cantidad de memoria de almacenamiento que desperdiciamos con algunas de "nuestras" decisiones técnicas :

  • Mantener instalaciones que NO utilizamos (versiones, dependencias extras, etc.)
  • Tener todo el SW actualizado a la última versión, aunque sólo lo hayamos utilizado una vez en la vida
  • Acumular todo tipo de ficheros, etc.
  • ...

Esto se debe principalmente a que actualmente conseguir memoria de almacenamiento o HW con cierta potencia es relativamente sencillo y que NO existe costumbre de optimizar nuestras cosas al máximo para que usen el 100% de los recursos de la mejor forma (que se lo digan a los primeros desarrolladores :-) )

Si esto ya nos pasa de forma normal en nuestro día a día "informático" en Docker esto no iba a ser menos. Una de sus principales características el "basarse en capas" NO nos ayuda, casi siempre acabamos con un contenedor que ocupa una barbaridad de MB para implementar un "Hello World", que por dentro hace un millón de cosas que no las necesitamos y eso sin contar lo que pesa el programita desarrollado...y no hablo del tiempo que hemos tardado en generarlo...jejeje

Pues existe una herramienta que se puede añadir a los comandos proporcionados por Docker (images, history, stats, etc.) que puede ayudar a mejorar todo esto ... Dive

Este artículo está dividido en 4 partes:

  • 1. Introducción
  • 2. Stack Tecnológico
  • 3. Ejemplos de Uso
  • 4. Conclusiones

1. Introducción

En este apartado se tratarán los siguiente puntos :

  • 1.1. Introducción al uso de contenedores como aceleradores del desarrollo
  • 1.2. Problemas generales derivados del uso de contenedores
  • 1.3. ¿Qué es Dive?
  • 1.4. ¿Qué necesidades tenemos para tener que usar "Dive"?

1.1. Introducción al uso de contenedores como aceleradores del desarrollo

Para centralizar esta información en un único punto y así poder facilitar su consulta se ha diseñado un artículo específico

1.2. Problemas generales derivados del uso de contenedores

Algunos de los problemas que nos encontramos a la hora de utilizar contenedores son los siguientes:

  • El problema de la optimización de recursos
  • El problema de la Dockerización y sus capas
  • El problema del diseño correcto de las imágenes
1.2.1. El problema de la optimización de recursos
Partimos de la base de que casi siempre los recursos HW son finitos porque en muchos casos no tenemos todo el dinero del mundo para invertirlo en ello

Para explicar bien este apartado primero de todo hay que hablar de máquinas virtuales (VMs) y contenedores

¿Qué es una máquina virtual (VM)?

Una máquina virtual es una emulación de un ordenador en un HW, es decir, es tener un "ordenador dentro de otro ordenador" para ello tenemos un ordenador que tiene su HW, su SO y sobre este instalamos la VM, a partir de ese momento la VM usa los recursos HW del otro ordenador como si fueran suyos (los toma prestados).

Importante : La VM no sabe que es una VM.

El Hypervisor es el software que se encarga de prestar los recursos a la VM.

Diagrama de estructura de un ordenador al hacer uso de VMs

drawing
¿Qué es un Contenedor?

Un contenedor es una "virtualización" sólo del sistema operativo, por lo tanto, cada uno de los contenedores NO tiene HW asignado de forma particular sino que se le asigna un parte fraccionada.

El Kernel esta compartido entre todos los contenedores.

Diagrama de estructura de un ordenador al hacer uso de contenedores

drawing
Gestión de recursos : VM vs. Contenedores

El uso de contenedores mejora la gestión de recursos frente al uso de máquinas virtuales (VM) a la hora de desplegar aplicaciones. Cada VM requiere tener su sistema operativo (SO) y tener un SO siempre implica gastar recursos. Por lo tanto, cuantos más SO operativos se tengan sobre una máquina anfitriona, mayor es el desperdicio de memoria.

Curiosidad: Algunas veces incluso las VMs se comportan tratando de apoderarse del total de los recursos de la máquina anfitriona y esto es un problema añadido...jejeje

En cambio, los contenedores NO tienen un SO como tal por lo tanto son capaces de optimizar más el uso de los recursos HW de la máquina anfitriona.

Que quiere decir todo esto, pues que a igualdad en cantidad de recursos HW se pueden desplegar más cantidad de contenedores que de VMs.

1.2.2. El problema de la Dockerización y sus capas
¿Qué es el proceso de "Dockerización" o "Dockerizar"?
Procedimiento basado en el empaquetado de una aplicación para posteriormente ser distribuida y ejecutada haciendo uso de los contenedores de SW

Para ello Docker proporciona 2 mecanismos para hacerlo:

  • "A mano" : Consiste en arrancar un contenedor con las características que quieras a nivel de plataforma de ejecución, para posteriormente proporcionarle la aplicación vía consola (scp, instalando git, etc), configurarlo dentro, establecer -> No es automatizable, existen muchas posibilidades de meter errores humanos, no es replicable, etc.
  • Se puede mejor usando volúmenes
  • "Mediante fichero 'Dockerfile'" : Se generar un fichero con un formato establecido y a partir de ahí se dan las instrucciones para su construcción e inclusión de la aplicación -> Automatizable, menos fallos humanos, requiere una correcta definición, replicable, etc.

Ejemplo de fichero DockerFile

FROM openjdk:8-jdk

ADD /target/acme-greeting-api-restful-0.0.1-SNAPSHOT.jar acme-greeting-api-restful-0.0.1-SNAPSHOT.jar

ENTRYPOINT ["java","-jar","acme-greeting-api-restful-0.0.1-SNAPSHOT.jar"]

EXPOSE 8091  

Se trata de un fichero Dockerfile de ejecución de aplicaciones (RUNTIME), donde se utiliza de base una imagen de la plataforma "openjdk:8-jdk", además se añade una aplicación de ejemplo desde el directorio "/target" del ordenador local , es decir, que previamente ha sido generada y posteriormente se ejecutará dentro del contenedor.

Tamaño analizado previamente :

  • Imagen base utilizada "openjdk:8-jdk" : 510 MB
  • Aplicación utilizada : 28,8 MB  (aprox. 29 MB)
  • TOTAL : aprox 539 MB

Cuando uno construye una imagen Docker se generan una serie de capas (layers) y cada capa esta representada por una instrucción ejecutada.

Ejemplo de una primera construcción de la imagen anterior

Comando ejecutado :

docker build -t acme/acme-greeting-api-restful .  

La primera vez que se ejecuta el FROM definido en el fichero, Docker comprueba si lo tiene guardado en su repositorio de imágenes locales y en caso de NO tenerla se la descarga desde Docker Hub u otro repositorio indicado.

Posteriormente termina de ejecutar el fichero

Se indican los pasos ejecutados que se corresponden con las instrucciones, es decir, se han generado 4 capas.

Se comprueba el tamaño de las imágenes montadas en local.

Comando ejecutado :

docker images  

La imagen base tiene un tamaño de 510M y la imagen base con la modificación de la aplicación tiene un tamaño 539MB

Se comprueba el historial de la imagen modificada

Comando ejecutado :

docker history <IMAGE_ID>  

Se puede observar el desglose de cada capa que lo forma y que tamaño lo incrementa cada capa.

Nota : Al incorporar la aplicación se añaden 28,8 MB

Ejemplo de construcción usando caché de la imagen anterior

Comando ejecutado :

docker build -t acme/acme-greeting-api-restful .  

Se puede ver que como ya tiene la imagen base descargada y las capas cacheadas el tiempo de construcción es mucho más reducido.

Cualquier imagen Docker siempre se basa en una imagen inicial -> Usa una capa Base

Que quiere decir esto, pues muy sencillo... que sobre una capa base que inicialmente tiene un tamaño específico,  le aplicamos una serie de modificaciones que van modificando la imagen, cada modificación genera una nueva capa encima de la anterior con lo que eso conlleva en algunos casos como el incremento de las funcionalidades y por lo tanto del tamaño.

Estas capas Docker las cachea como ya se ha podido ver, por lo que la próxima vez que la ejecute será más rápido y se vuelves a necesitar las mismas capas pues la generación de la imagen será todavía más rápida.

  • Si modificamos la última capa sería lo mejor que podría pasa, porque la generaría y podría reutilizar el resto de las capas desde la caché.
  • Si modificamos la capa base o una de las capas intermedias la cosa cambia un poco, desde la capa que se modifica se vuelve a generar cada una de las capas restantes perdiendo la utilidad de la caché.
1.2.3. El problema del diseño correcto de las imágenes

Existen una serie de criterios y buenas prácticas a la hora de diseñar el fichero Dockerfile que permite "Dockerizar" una aplicación

  • Elegir el planteamiento : standalone-state (one-state) o multi-stage
  • standalone-stage : utilizará una única imagen sobre la que trabajar
  • multi-stage : utilizará varias imágenes donde la salida de una de ellas será la entrada de la otra
  • Elegir el enfoque de uso de la imagen
  • Tipos : Build, Runtime, Build+Runtime, ...
  • Elegir de forma correcta la imagen base
  • Utilizar siempre una imagen que "simule" el entorno en el que se va a ejecutar (Mismo SO, mismo tipo de JDK, mismas dependencias, etc.)
  • Utilizar imágenes y elementos con versionados específicos para mantener inmutabilidad
  • Utilizar siempre que se pueda imágenes mínimas ("sabor" reducido) : slim, alpine, etc. -> reducirá el consumo de espacio
  • No reinventar la rueda, ver si alguien "oficial" (Docker Hub) ya ha creado una imagen que haga lo que necesitemos, esta ya estará probada y optimizada :-)
  • Utilizar imágenes que tengan lo mínimo e imprescindible para funcionar (por ejemplo que no tenga dependencias innecesarias que lo único que hacen es consumir espacio, etc.)
  • Tener una correcta estructuración de las capas
  • Cada operación que se realiza genera una capa
  • Agrupar operaciones en una única -> generaría una única capa
  • Aquello que cambie mucho mejor situarlo en capas externa
  • Tener en cuenta el uso de caché de Docker en las construcciones
  • Acelerar los desarrollos al reducir los tiempos de construcción de la imagen
  • Se invalida la caché de Docker cada vez que se copia el código y este ha cambiado -> recordar que lo que cambie mucho debería de estar situado en las capas externas
  • Elegir la forma de trabajar con el proyecto dentro de la imagen
  • Replicar el proyecto completo (Por ejemplo: un proyecto entero puede incluir hasta el README.md, etc)
  • Replicar partes del proyecto (Por ejemplo: un proyecto entero puede excluir el README.md, etc)
  • Replicar el artefacto (Por ejemplo: un fichero JAR generado en una construcción)
  • Preparar la definición del proyecto dentro de la imagen
  • Cargar ficheros de propiedades externos
  • Establecer algún parámetro de construcción : perfil , etc.
  • Si se realiza algún tipo de construcción tener en cuenta aspectos como la desactivación de los test, etc
  • ....
  • Generalidades
  • Instalar y configurar sólo aquello que se vaya a utilizar
  • Evitar las actualizaciones (de la imagen base, de las dependencias utilizadas, etc.)
  • Tratar de agrupar actualizaciones e instalaciones en una sola línea (apt-get update && apt-get -y install xxx yyy ...)
  • Evitar instalar recomendaciones (Por ejemplo : apt-get -y install --no-install-recommends)
  • Tener en cuenta que los proyectos tienen dependencias internas y externas asociadas y esto puede incrementar el tamaño de la imagen
  • Eliminar aquello que NO sea necesario (Por ejemplo : las listas de paquetes con rm -ref /var/lib/apt/lists/*)
  • Tratar de optimizar tiempos de construcción y tamaños
  • ...

Cuanto menor sea el tamaño de la imagen mejor será la optimización de recursos, mejor será la transferencia por la red, menor sera el almacenamiento, se facilitarán los despliegues en CI CD, etc.

Ejemplo de fichero DockerFile utilizando una versión SLIM de la JDK de la imagen base

FROM openjdk:8-slim

ADD /target/acme-greeting-api-restful-0.0.1-SNAPSHOT.jar acme-greeting-api-restful-0.0.1-SNAPSHOT.jar

ENTRYPOINT ["java","-jar","acme-greeting-api-restful-0.0.1-SNAPSHOT.jar"]

EXPOSE 8091  

Tamaño :

  • Imagen base utilizada "openjdk:8-slim" : 285 MB
  • Aplicación utilizada : 28,8 MB  (aprox. 29 MB)
  • TOTAL : aprox 313 MB

Ejemplo de fichero DockerFile utilizando una versión SLIM de la JRE de la imagen base

FROM openjdk:8-jre-slim

ADD /target/acme-greeting-api-restful-0.0.1-SNAPSHOT.jar acme-greeting-api-restful-0.0.1-SNAPSHOT.jar

ENTRYPOINT ["java","-jar","acme-greeting-api-restful-0.0.1-SNAPSHOT.jar"]

EXPOSE 8091  

Tamaño :

  • Imagen base utilizada "openjdk:8-jre-slim" : 184 MB
  • Aplicación utilizada : 28,8 MB  (aprox. 29 MB)
  • TOTAL : aprox 213 MB

Han sido dos ejemplos de optimización muy rápidos basados en elegir una buena imagen base y ya se ha podido ver la memoria que se ha podido descargar.

Esto se podrá aplicar a todos los entornos y sobre todo en Cloud donde se paga por lo que se usa lo que se traduce en un ahorro de costes importantes o un dolor de cabeza a nivel económico sino se trata desde un inicio.

1.3. ¿Qué es Dive?

Dive es una herramienta Open Source utilizada para analizar en detalle las capas de una imagen Docker y así ayudar a optimizar el uso de sus recursos al avisar de que ficheros se consideran que "no aporta" (ineficientes)

Tiene las siguientes características :

  • Open sources
  • Facilidad a la hora de instalarlo en diferentes entornos/plataformas (Ubuntu/Debian, RHEL/Centos, Arch Linux, Mac, Windows y Docker)
  • Ejecución por "línea de comandos" (su UI la define mediante consola -> Al principio parece intimidante pero luego tras usarla es bastante "resultona")
  • Muestra la estructura de capas de una imagen Docker, las instrucciones ejecutadas en cada una de ellas
  • Muestra el estado o situación de los directorios y ficheros de la imagen en cada una de las capas
  • Determina los puntos de mejora de "eficiencia" identificando aquellos ficheros que pueden ser candidatos que mejorar
  • Proporciona ciertas facilidades a la hora de hacer los análisis
  • Proporciona diferentes soportes a nivel de las imágenes y tecnologías de Contenedores: Docker y Podman
  • Configurable vía fichero de configuración propia y según el análisis mediante parámetros
  • Facilidad para integrarlo en un ciclo de CI
  • ...

1.4. ¿Qué necesidades tenemos para tener que usar "Dive"?

Los motivos principales que me llevaron a tratar de investigar sobre la optimización de contenedores fueron los siguientes :

  • Disponer de una herramienta que me ayudara a analizar y a entender qué estaba pasando en cada capa. Y mucho más cuando son capas que yo no he añadido, así que usaremos "ingeniería inversa"
  • Tratar de disponer de una imagen con un consumo mínimo a nivel de tamaño, entendiendo que es lo mínimo y necesario que necesitaban mis microservicios para funcionar (en mi caso los microservicios estan hechos con Java 8 / Spring Boot)
  • Reducir los costes de tiempo de construcción en mi ciclo de CI (sobre todo Jenkins)
  • Reducir los costes de almacenamiento en mi ciclo de CI (sobre todo a la hora de mantener versionado de las imágenes de forma persistida en Nexus o en Azure Container Registry)
  • Reducir los costes de infraestructura del Cloud los cuales cobran por transferencia y almacenamiento
  • Tratar de integrar dicha herramienta en mi ciclo de CI -> si la imagen no cambia a nivel de plataforma con un análisis previo valdría, pero si utilizara imágenes que tuvieran comandos de actualización o de instalación de últimos productos podría interesar realizar el análisis.

2. Stack Tecnológico

Este es stack tecnológico elegido para implementar la funcionalidad "Optimizando imágenes Docker con Dive":

  • Java 8
  • Docker - Tecnología de Contenedores/Containers
  • Docker Hub - Repositorio de Docker Público donde se ubican las imágenes oficiales
  • Dive - Herramienta de optimización de imágenes

3. Ejemplos de Uso

Para enseñar a utilizarlo y así practicar se hará uso de la herramienta contra el ejemplo de Dockerfile explicado en apartados anteriores

Nota: En este caso NO existirá un repositorio asociado

La interfaz por consola que habilita la herramientas es la siguiente:

La pantalla se divide en 4 partes :

Sección [Layers]

Se sitúa en la parte de arriba a la izquierda y muestra cada una de las capas ordenadas por su generación

  • Permite seleccionar una de las capas para el análisis

Sección [Layer Details]

Se sitúa en la parte central a la izquierda y muestra el detalle de lo que se ha hecho sobre la capa seleccionada

  • Muestra la información de la capa

Sección [Image Details]

Se sitúa en la parte baja a la izquierda y muestra el resumen del tamaño de la imagen, su eficiencia (potential wasted space) además de un listado de ficheros considerados como ineficientes

Sección [Current Layer Contents]

Se sitúa en la parte de la derecha y muestra el listado de la estructura de directorios / ficheros que se encuentran dentro de la imagen

3.1. Scripts de Soporte

N/A

3.2. Investigar lo que se usa

N/A

3.3. Ejecución desde una instalación Standalone

Nota: Se ha realizado una instalación sobre la plataforma Mac

Comando ejecutado (si la imagen ya esta creada) :

dive <IMAGE_TAG>  

Comando ejecutado (si la imagen la tiene que construir previamente) :

dive build -t <IMAGE_TAG> .  

Ejemplo de ejecución para una de mis imágenes: acme/acme-greeting-api-restful

dive build -t acme/acme-greeting-api-restful .  

3.4. Ejecución desde una instalación Docker

Descargar el contenedor (mejor si establecemos una versión) :

docker pull wagoodman/dive:v0.9  

Comando ejecutado (si la imagen ya esta creada) :

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:v0.9 <IMAGE_TAG> <DIVE_ARGUMENTS>  

Ejemplo de ejecución para una de mis imágenes : acme/acme-greeting-api-restful

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:v0.9 acme/acme-greeting-api-restful  

3.5. Integración en el ciclo de CI

Según la documentación de Dive, esta herramienta se puede configurar para incorporar al ciclo de CI un análisis de las imágenes SIN que se establezca la fase de auditoría, de forma que se establezcan unos criterios de evaluación del análisis de la imagen previamente:

  • via parámetros
  • via fichero

Comando de ejecución si se usa la instalación sobre plataforma en modo CI

CI=true dive <IMAGE_TAG>  

Se requiere el indicador mediante la variable de entorno CI

Ejemplo del resultado de la ejecución anterior :

Comando de ejecución si se usa la instalación sobre Docker con parámetros de auditoria establecidos como argumentos

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest --ci <IMAGE_TAG> --lowestEfficiency=0.8 --highestUserWastedPercent=0.45  

Ejemplo del resultado de la ejecución anterior :

Con el parámetro --ci-config sele puede pasar al comando anterior y así cambiar el paso de argumentos por el uso de los valores de un fichero que se ubica en .dive-ci

rules:  
  # If the efficiency is measured below X%, mark as failed.
  # Expressed as a ratio between 0-1.
  lowestEfficiency: 0.95

  # If the amount of wasted space is at least X or larger than X, mark as failed.
  # Expressed in B, KB, MB, and GB.
  highestWastedBytes: 20MB

  # If the amount of wasted space makes up for X% or more of the image, mark as failed.
  # Note: the base image layer is NOT included in the total image size.
  # Expressed as a ratio between 0-1; fails if the threshold is met or crossed.
  highestUserWastedPercent: 0.20

Pero esto del fichero ya lo dejo para que lo configuréis vosotros vosotros :-)

4. Conclusiones

Pues con esto ya tenemos el objetivo del artículo cubierto: dispondremos de imágenes optimizadas "al máximo" según nuestras necesidades, ademas de haber aprendido a hacer un poco de "buceo de profundidad" entrando en algunos detalles que normalmente dejamos de lado o a los que no damos importancia hasta que aparece un problema (esto pasa siempre) sobre instalación, configuración o uso.

Además, se puede añadir como una fase más dentro del ciclo de CI/CD que tengamos que podría tener mucho más sentido si nuestras imágenes pueden sufrir algún tipo de actualización o upgrade de alguna de sus partes

Si además de todo lo anterior, somos capaces de añadir algún tipo de análisis de seguridad sobre la imagen utilizadas habremos subido un +5 en experiencia en la forma de desarrollar contenedores "profesionales" :-)

Nos vemos en próximos artículos y si te ha gustado, ¡síguenos en Twitter para estar al día de próximos posts!

Autor

Víctor Madrid

Líder Técnico de la Comunidad de Arquitectura de Soluciones en atSistemas. Aprendiz de mucho y maestro de nada. Técnico, artista y polifacético a partes iguales ;-)