Compartir comentarios
Las respuestas se generan en base a la documentación.

Dominar compilaciones multiplataforma, pruebas y más con Docker Buildx Bake

Esta guía muestra cómo simplificar y automatizar el proceso de compilar imágenes, ejecutar pruebas y generar artefactos de compilación con Docker Buildx Bake. Al definir configuraciones de compilación en un archivo declarativo docker-bake.hcl, puedes eliminar scripts manuales y habilitar flujos eficientes para compilaciones complejas, pruebas y generación de artefactos.

Suposiciones

Esta guía asume que conoces:

Requisitos previos

  • Tienes una versión reciente de Docker instalada en tu máquina.
  • Tienes Git instalado para clonar repositorios.
  • Usas el almacén de imágenes containerd.

Introducción

Esta guía usa un proyecto de ejemplo para demostrar cómo Docker Buildx Bake puede agilizar tus flujos de compilación y prueba. El repositorio incluye un Dockerfile y un archivo docker-bake.hcl, con una configuración lista para probar comandos de Bake.

Empieza clonando el repositorio de ejemplo:

git clone https://github.com/dvdksn/bakeme.git
cd bakeme

El archivo Bake, docker-bake.hcl, define los targets de compilación con sintaxis declarativa, usando targets y grupos, lo que te permite gestionar compilaciones complejas de forma eficiente.

Así es el archivo Bake tal como viene:

target "default" {
  target = "image"
  tags = [
    "bakeme:latest",
  ]
  attest = [
    "type=provenance,mode=max",
    "type=sbom",
  ]
  platforms = [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
  ]
}

La palabra clave target define un target de compilación para Bake. El target default define el target a compilar cuando no se especifica ninguno en la línea de comandos. Resumen de las opciones del target default:

  • target: La etapa de compilación del target en el Dockerfile.

  • tags: Etiquetas que se asignan a la imagen.

  • attest: Attestations que se adjuntan a la imagen.

    Tip

    Las attestations aportan metadatos como la procedencia de la compilación, que registra el origen de la compilación de la imagen, y un SBOM (Software Bill of Materials), útil para auditorías de seguridad y cumplimiento.

  • platforms: Variantes de plataforma a compilar.

Para ejecutar esta compilación, ejecuta el siguiente comando en la raíz del repositorio:

$ docker buildx bake

Con Bake evitas encantamientos largos y difíciles de recordar en la línea de comandos, simplificando la gestión de la configuración de compilación al sustituir scripts manuales propensos a errores por un archivo de configuración estructurado.

Para contrastar, así sería este comando de compilación sin Bake:

$ docker buildx build \
  --target=image \
  --tag=bakeme:latest \
  --provenance=true \
  --sbom=true \
  --platform=linux/amd64,linux/arm64,linux/riscv64 \
  .

Pruebas y linting

Bake no sirve solo para definir configuraciones de compilación y ejecutar builds. También puedes usar Bake para ejecutar tus pruebas, usando BuildKit efectivamente como ejecutor de tareas. Ejecutar pruebas en contenedores es ideal para resultados reproducibles. Esta sección muestra cómo añadir dos tipos de pruebas:

  • Pruebas unitarias con go test.
  • Linting de violaciones de estilo con golangci-lint.

Al estilo de desarrollo guiado por pruebas (TDD), empieza añadiendo un target test al archivo Bake:

target "test" {
  target = "test"
  output = ["type=cacheonly"]
}
Tip

Usar type=cacheonly garantiza que la salida de la compilación se descarte en la práctica; las capas se guardan en la caché de BuildKit, pero Buildx no intentará cargar el resultado en el almacén de imágenes del Docker Engine.

En ejecuciones de prueba no necesitas exportar la salida de la compilación; solo importa la ejecución de las pruebas.

Para ejecutar este target de Bake, ejecuta docker buildx bake test. Por ahora recibirás un error indicando que la etapa test no existe en el Dockerfile.

$ docker buildx bake test
[+] Building 1.2s (6/6) FINISHED
 => [internal] load local bake definitions
...
ERROR: failed to solve: target stage "test" could not be found

Para satisfacer este target, añade el target correspondiente en el Dockerfile. La etapa test aquí se basa en la misma etapa base que la etapa de compilación.

FROM base AS test
RUN --mount=target=. \
    --mount=type=cache,target=/go/pkg/mod \
    go test .
Tip

La directiva --mount=type=cache almacena en caché los módulos de Go entre compilaciones, mejorando el rendimiento al evitar volver a descargar dependencias. Esta caché compartida garantiza que el mismo conjunto de dependencias esté disponible en compilación, pruebas y otras etapas.

Ahora, ejecutar el target test con Bake evaluará las pruebas unitarias de este proyecto. Si quieres comprobar que funciona, puedes hacer un cambio arbitrario en main_test.go para que la prueba falle.

A continuación, para habilitar el linting, añade otro target al archivo Bake, llamado lint:

target "lint" {
  target = "lint"
  output = ["type=cacheonly"]
}

Y en el Dockerfile, añade la etapa de compilación. Esta etapa usará la imagen oficial golangci-lint en Docker Hub.

Tip

Como esta etapa depende de ejecutar una dependencia externa, suele ser buena idea definir la versión que quieres usar como argumento de compilación. Así puedes gestionar actualizaciones de versión más fácilmente en el futuro concentrando las versiones de dependencias al inicio del Dockerfile.

ARG GO_VERSION="1.23"
ARG GOLANGCI_LINT_VERSION="1.61"

#...

FROM golangci/golangci-lint:v${GOLANGCI_LINT_VERSION}-alpine AS lint
RUN --mount=target=.,rw \
    golangci-lint run

Por último, para ejecutar ambas pruebas a la vez, puedes usar el constructo groups en el archivo Bake. Un grupo puede especificar varios targets para ejecutar con una sola invocación.

group "validate" {
  targets = ["test", "lint"]
}

Ahora, ejecutar ambas pruebas es tan simple como:

$ docker buildx bake validate

Variantes de compilación

A veces necesitas compilar más de una versión de un programa. El siguiente ejemplo usa Bake para compilar variantes separadas de «release» y «debug» del programa, usando matrices. Las matrices te permiten ejecutar compilaciones en paralelo con distintas configuraciones, ahorrando tiempo y garantizando consistencia.

Una matriz expande una sola compilación en varias, cada una representando una combinación única de parámetros de matriz. Así puedes orquestar Bake para compilar en paralelo la versión de producción y la de desarrollo de tu programa con cambios mínimos de configuración.

El proyecto de ejemplo de esta guía está configurado para usar una opción en tiempo de compilación que activa condicionalmente capacidades de registro y trazas de depuración.

  • Si compilas el programa con go build -tags="debug", se habilitan el registro y las trazas adicionales (modo desarrollo).
  • Si compilas sin la etiqueta debug, el programa se compila con un logger por defecto (modo producción).

Actualiza el archivo Bake añadiendo un atributo matrix que define las combinaciones de variables a compilar:

docker-bake.hcl
 target "default" {
+  matrix = {
+    mode = ["release", "debug"]
+  }
+  name = "image-${mode}"
   target = "image"

El atributo matrix define las variantes a compilar («release» y «debug»). El atributo name define cómo la matriz se expande en varios targets de compilación distintos. En este caso, el atributo matrix expande la compilación en dos flujos: image-release e image-debug, cada uno con distintos parámetros de configuración.

A continuación, define un argumento de compilación llamado BUILD_TAGS que tome el valor de la variable de matriz.

docker-bake.hcl
   target = "image"
+  args = {
+    BUILD_TAGS = mode
+  }
   tags = [

También conviene cambiar cómo se asignan las etiquetas de imagen a estas compilaciones. Actualmente, ambas rutas de la matriz generarían los mismos nombres de etiqueta y se sobrescribirían. Actualiza el atributo tags para usar un operador condicional que fije la etiqueta según el valor de la variable de matriz.

docker-bake.hcl
   tags = [
-    "bakeme:latest",
+    mode == "release" ? "bakeme:latest" : "bakeme:dev"
   ]
  • Si mode es release, el nombre de etiqueta es bakeme:latest
  • Si mode es debug, el nombre de etiqueta es bakeme:dev

Por último, actualiza el Dockerfile para consumir el argumento BUILD_TAGS durante la etapa de compilación. Cuando la opción -tags="${BUILD_TAGS}" se evalúa como -tags="debug", el compilador usa la función configureLogging en el archivo debug.go.

Dockerfile
 # build compiles the program
 FROM base AS build
-ARG TARGETOS TARGETARCH
+ARG TARGETOS TARGETARCH BUILD_TAGS
 ENV GOOS=$TARGETOS
 ENV GOARCH=$TARGETARCH
 RUN --mount=target=. \
        --mount=type=cache,target=/go/pkg/mod \
-       go build -o "/usr/bin/bakeme" .
+       go build -tags="${BUILD_TAGS}" -o "/usr/bin/bakeme" .

Eso es todo. Con estos cambios, tu comando docker buildx bake compila ahora dos variantes de imagen multiplataforma. Puedes inspeccionar la configuración de compilación canónica que genera Bake con el comando docker buildx bake --print. Al ejecutarlo verás que Bake ejecutará un grupo default con dos targets con distintos argumentos de compilación y etiquetas de imagen.

{
  "group": {
    "default": {
      "targets": ["image-release", "image-debug"]
    }
  },
  "target": {
    "image-debug": {
      "attest": ["type=provenance,mode=max", "type=sbom"],
      "context": ".",
      "dockerfile": "Dockerfile",
      "args": {
        "BUILD_TAGS": "debug"
      },
      "tags": ["bakeme:dev"],
      "target": "image",
      "platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
    },
    "image-release": {
      "attest": ["type=provenance,mode=max", "type=sbom"],
      "context": ".",
      "dockerfile": "Dockerfile",
      "args": {
        "BUILD_TAGS": "release"
      },
      "tags": ["bakeme:latest"],
      "target": "image",
      "platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
    }
  }
}

Teniendo en cuenta también todas las variantes de plataforma, la configuración de compilación genera 6 imágenes distintas.

$ docker buildx bake
$ docker image ls --tree

IMAGE                   ID             DISK USAGE   CONTENT SIZE   USED
bakeme:dev              f7cb5c08beac       49.3MB         28.9MB
├─ linux/riscv64        0eae8ba0367a       9.18MB         9.18MB
├─ linux/arm64          56561051c49a         30MB         9.89MB
└─ linux/amd64          e8ca65079c1f        9.8MB          9.8MB

bakeme:latest           20065d2c4d22       44.4MB         25.9MB
├─ linux/riscv64        7cc82872695f       8.21MB         8.21MB
├─ linux/arm64          e42220c2b7a3       27.1MB         8.93MB
└─ linux/amd64          af5b2dd64fde       8.78MB         8.78MB

Exportar artefactos de compilación

Exportar artefactos de compilación como binarios puede ser útil para desplegar en entornos sin Docker o Kubernetes. Por ejemplo, si tus programas deben ejecutarse en la máquina local del usuario.

Tip

Las técnicas de esta sección se aplican no solo a la salida de compilación como binarios, sino a cualquier tipo de artefacto, como informes de pruebas.

Con lenguajes como Go y Rust, donde los binarios compilados suelen ser portables, crear targets de compilación alternativos para exportar solo el binario es sencillo. Solo necesitas añadir una etapa vacía en el Dockerfile que contenga únicamente el binario que quieres exportar.

Primero, añade una forma rápida de compilar un binario para tu plataforma local y exportarlo a ./build/local en el sistema de archivos local.

En el archivo docker-bake.hcl, crea un target bin nuevo. En esta etapa, establece el atributo output en una ruta del sistema de archivos local. Buildx detecta automáticamente que la salida parece una ruta de archivo y exporta los resultados a la ruta indicada usando el exportador local.

target "bin" {
  target = "bin"
  output = ["build/bin"]
  platforms = ["local"]
}

Observa que esta etapa especifica la plataforma local. Por defecto, si platforms no se especifica, las compilaciones apuntan al SO y la arquitectura del host de BuildKit. Si usas Docker Desktop, esto suele significar compilaciones para linux/amd64 o linux/arm64, aunque tu máquina local sea macOS o Windows, porque Docker se ejecuta en una VM Linux. Usar la plataforma local fuerza que la plataforma objetivo coincida con tu entorno local.

A continuación, añade la etapa bin al Dockerfile que copia el binario compilado desde la etapa de compilación.

FROM scratch AS bin
COPY --from=build "/usr/bin/bakeme" /

Ahora puedes exportar la versión de tu plataforma local del binario con docker buildx bake bin. Por ejemplo, en macOS, este target de compilación genera un ejecutable en formato Mach-O, el formato ejecutable estándar de macOS.

$ docker buildx bake bin
$ file ./build/bin/bakeme
./build/bin/bakeme: Mach-O 64-bit executable arm64

A continuación, añade un target para compilar todas las variantes de plataforma del programa. Para ello, puedes heredar el target bin que acabas de crear y ampliarlo añadiendo las plataformas deseadas.

target "bin-cross" {
  inherits = ["bin"]
  platforms = [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
  ]
}

Ahora, compilar el target bin-cross crea binarios para todas las plataformas. Se crean subdirectorios automáticamente para cada variante.

$ docker buildx bake bin-cross
$ tree build/
build/
└── bin
    ├── bakeme
    ├── linux_amd64
    │   └── bakeme
    ├── linux_arm64
    │   └── bakeme
    └── linux_riscv64
        └── bakeme

5 directories, 4 files

Para generar también variantes «release» y «debug», puedes usar una matriz igual que con el target por defecto. Al usar una matriz, también debes diferenciar el directorio de salida según el valor de la matriz; si no, el binario se escribe en la misma ubicación en cada ejecución de la matriz.

target "bin-all" {
  inherits = ["bin-cross"]
  matrix = {
    mode = ["release", "debug"]
  }
  name = "bin-${mode}"
  args = {
    BUILD_TAGS = mode
  }
  output = ["build/bin/${mode}"]
}
$ rm -r ./build/
$ docker buildx bake bin-all
$ tree build/
build/
└── bin
    ├── debug
    │   ├── linux_amd64
    │   │   └── bakeme
    │   ├── linux_arm64
    │   │   └── bakeme
    │   └── linux_riscv64
    │       └── bakeme
    └── release
        ├── linux_amd64
        │   └── bakeme
        ├── linux_arm64
        │   └── bakeme
        └── linux_riscv64
            └── bakeme

10 directories, 6 files

Conclusión

Docker Buildx Bake agiliza flujos de compilación complejos, permitiendo compilaciones multiplataforma, pruebas y exportación de artefactos de forma eficiente. Al integrar Buildx Bake en tus proyectos, puedes simplificar tus compilaciones Docker, hacer portable tu configuración de compilación y dominar configuraciones complejas con más facilidad.

Experimenta con distintas configuraciones y amplía tus archivos Bake según las necesidades de tu proyecto. Puedes integrar Bake en tus pipelines de CI/CD para automatizar compilaciones, pruebas y despliegue de artefactos. La flexibilidad y potencia de Buildx Bake pueden mejorar significativamente tus procesos de desarrollo y despliegue.

Lecturas adicionales

Para más información sobre cómo usar Bake, consulta estos recursos: