Cómo crear una imagen docker multiarquitectura desde cero

Tiempo de lectura: 7 minutos
Imagen ornamental de docker

Estas últimas semanas han sido apasionantes, Kubecon, DotNetConf,… Un montón de novedades y nuevas versiones de diferentes softwares y herramientas de las que hablaremos en las próximas semanas. Hoy vengo a hablar de algo más ‘mundano’, como generar imágenes Docker. Solo que esta vez vamos a hacer que nuestra imagen sea multiarquitectura.

DISCLAIMER: La idea de esta entrada es hablar de cómo crear imágenes Docker multiarquitectura, por lo que no se entra en los detalles sobre crear imágenes Docker en si mismo, sino que se pasa por encima para dar el contexto.

¿Qué es y cómo funciona Docker?

Antes de empezar a hablar sobre imágenes multiarquitectura, conviene empezar aclarando que es Docker y como funciona. Docker es un sistema de empaquetado de contenedores que nos permite añadir aplicaciones y dependencias en un mismo ‘paquete’, de modo que corra donde corra, siempre funcionará igual. Podríamos decir que es como si cogiéramos una máquina virtual y la distribuyésemos. De este modo, podemos preparar el sistema operativo, sus dependencias y nuestra aplicación, generar un ‘paquete’ y ponerlo en marcha allá donde queramos.

Con esto mente, hay que aclarar que docker es como correr una maquina virtual, pero no es correr una máquina virtual. Esto quiere decir que, a diferencia de una máquina virtual, aquí no movemos todo el sistema operativo, sino simplemente las ‘particularidades’ que tiene, dejando la base común intacta ya que esta base común se comparte con el sistema operativo anfitrión.

La imagen muestra la composición de las aplicaciones en una VM y en Docker, donde en la VM el sistema huésped corre sobre el sistema anfitrión y sobre este huésped las aplicaciones mientras que en Docker las aplicaciones corren sobre el contenedor y este sobre el sistema anfitrión

A esta tecnología se la conoce como contenedores y no es algo nuevo, si no que lleva existiendo varios años ya. Su funcionamiento (simplificado) es que vamos a crear imágenes que con las dependencias y aplicaciones, y estas son las que vamos a distribuir. Precisamente del hecho de que la parte común se comparta con el sistema operativo anfitrión, es por lo que tenemos que hablar de imágenes Docker para una arquitectura concreta. Por poner un símil, diferentes distribuciones de Linux comparten la misma base común que es el kernel de Linux, diferentes imágenes comparten la misma arquitectura.

¿Por qué me puede interesar crear imágenes Docker multiarquitectura?

En las últimas entradas estuvimos hablando sobre módulos para IoT Edge y sobre aprovisionamiento de dispositivos con IoT Hub. Pues bien, en ese momento no entramos en detalle, aunque hicimos referencia a ello:

Actualmente, Docker soporta de forma experimental el crear manifiestos de imágenes para múltiples arquitecturas simultáneamente, siendo el runtime de Docker el que se descarga la imagen de su arquitectura especifica.

Creando módulos especializados para Azure IoT Edge

Pues efectivamente, este es un caso muy claro donde seguramente nos interese crear imágenes Docker multiarquitectura. Esto es porque es un escenario común que queramos crear módulos de IoT Edge que trabajen por ejemplo de igual manera si los desplegamos en un microprocesador amd64, arm32, arm64, …

Estos escenarios solían requerir de diferentes imágenes con una imagen base diferente, y cuyo uso requería de mantener un tag diferente para identificarlos. De este modo no es nada raro encontrarnos con que muchos repositorios de Docker Hub tienen soporte especifico mediante tags para diferentes arquitecturas.

Esto tiene un problema evidente y es que te obliga a especificar un tag concreto en el que indicamos la arquitectura, y dificulta mucho el generalizar en situaciones de despliegue masivo. ¿Realmente necesito saber la arquitectura del dispositivo IoT Edge para ejecutar el un módulo? ¿Realmente es algo que quiera tener en cuenta?

En Python sin ir más lejos, su imagen soporta hasta 10 arquitecturas distintas:

En cambio, todas ellas se sirven desde el mismo tag, es decir, están agrupadas y es el propio cliente de Docker el que se baja la imagen concreta que necesita para el caso concreto.

Creando nuestra primera imagen Docker multiarquitectura (Docker Manifest)

Vamos a partir plantear un ejemplo muy sencillo. Partimos de un escenario donde todas las arquitecturas que queremos soportar ya están soportadas por la imagen base que utilicemos.

Lo primero que vamos a necesitar, es habilitar el soporte para ‘Docker Manifest‘. Esta es una característica experimental de Docker por lo que para habilitarlas debemos (extraído y traducido de la documentación oficial):

Para habilitar las características experimentales en el Docker CLI, edite el archivo config.json y ponga experimental a habilitado.
Para habilitar las características experimentales en el menú del escritorio de la base Docker, haz clic en Configuración (Preferencias en macOS) > Línea de comandos y luego activa la opción Habilitar características experimentales. Haz clic en Aplicar y reiniciar.

Una vez teniendo eso listo, vamos a crear las diferentes imágenes que queremos que se sirvan desde nuestra ‘meta imagen’ multiarquitectura. Por ejemplo, podríamos tener un par de Dockerfiles como estos (en el repositorio de Github puedes encontrar más arquitecturas):

# Dockerfile.amd64
FROM amd64/alpine
CMD echo "Hello world desde amd64/alpine"

# Dockerfile.arm32v6
FROM arm32v6/alpine
CMD echo "Hello world desde arm32v6/alpine"

Una vez que tenemos estos dos Dockerfile, vamos a generar sus respectivas imágenes y subirlas a Docker Hub con los comandos:

# Generamos las imágenes
docker build -f .\Dockerfile.amd64 -t fixedbuffer/multi-arch:amd64  .
docker build -f .\Dockerfile.arm32v6 -t fixedbuffer/multi-arch:arm32v6  .

# Pusheamos las imágenes a Docker Hub
docker push fixedbuffer/multi-arch:amd64
docker push fixedbuffer/multi-arch:arm32v6

Una vez que hemos subido las imágenes, vamos a crear nuestro manifiesto que genere esa anhelada imagen Docker multiarquitectura. Para ello simplemente vamos a ejecutar el comando ‘docker manifest create‘ indicandole el nombre del manifiesto y su tag (que será el nombre de esa ‘meta imagen’) y las imágenes que queramos que la compongan:

docker manifest create fixedbuffer/multi-arch:latest fixedbuffer/multi-arch:amd64 fixedbuffer/multi-arch:arm32v6

En caso de que queramos añadir nuevas imágenes, basta con ejecutar de nuevo el comando con el modificador ‘–amend‘. Una vez que hemos terminado de crear nuestro manifiesto, basta con subirlo a Docker Hub ejecutando:

docker manifest push docker.io/fixedbuffer/multi-arch:latest

Si ahora vamos a Docker Hub, vamos a encontrarnos con que nuestro repositorio tiene una nueva imagen multiarquitectura son la misma etiqueta que nuestro manifiesto. Esta nueva imagen soporta varias arquitecturas:

Si ahora ejecutamos la imagen en diferentes equipos, vamos a poder comprobar como dependiendo del sistema, se utiliza de manera transparente una u otra imagen:

La imagen muestra el comando docker run desde una raspberry donde la salida es Hello world desde arm32v6/alpine
La imagen muestra el comando docker run desde un equipo Windows con Docker for Descktop donde la salida es Hello world desde amd64/alpine

Creando nuestra segunda imagen Docker multiarquitectura (Docker Buildx)

Si bien es cierto que esto funciona y funciona bien, se queda un poco a medias en el sentido de que es necesario generar las diferentes imágenes previamente a hacer el manifiesto. Esto significa que vamos a necesitar tener hardware que pueda ejecutar los Dockerfiles y subirlos a Docker Hub.

Otra opción, que personalmente me gusta más por ser muy sencilla de automatizar es 'docker buildx. Este comando nos va a permitir definir las diferentes arquitecturas que queremos que soporte nuestra imagen multiarquitectura, y será suficiente con que recibamos ese argumento en nuestro Dockerfile. Por ejemplo:

ARG ARCH=
FROM ${ARCH}debian:buster-slim

CMD echo "Hello world desde ${ARCH}debian:buster-slim"

Este Dockerfile simplemente recibe como parámetro el tipo de arquitectura que tiene que utilizar, el cual obtendrá desde la propia ejecución del comando:

docker buildx build --push --tag fixedbuffer/multi-arch:action --platform linux/amd64,linux/arm/v7,linux/arm64 -f Dockerfile.multi .

El comando ‘docker buildx build‘ soporta los mismos parámetros que soporta ‘docker build‘ de manera normal, pero además nos va a permitir utilizar algunos adicionales como –push para subir la imagen al terminar, o –platform para indicarle las diferentes plataformas que queremos soportar.

Una de las grandes ventajas que le veo respecto a Docker Manifest, es que es muy fácil de automatizar, como se puede comprobar en la entrada del propio blog de Docker (en inglés): Multi-arch build and images, the simple way – Docker Blog

Conclusión

En esta entrada hemos planteado un par de soluciones para poder generar imágenes Docker multiarquitectura, de modo que podamos utilizar la misma ‘meta imagen’ en todos nuestros dispositivos, sin tener que preocuparnos de cuál sea la arquitectura de dicho dispositivo.

Si bien es cierto que no es un escenario muy habitual, ya que normalmente las aplicaciones tienen una plataforma objetivo, no está de más conocer este tipo de posibilidades ya que como a mí, pueden sacarte de un apuro cuando generalizas las imágenes Docker.

Deja un comentario