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 bakemeEl 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.TipLas 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"]
}TipUsar
type=cacheonlygarantiza 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 .TipLa directiva
--mount=type=cachealmacena 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.
TipComo 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 runPor ú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:
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.
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.
tags = [
- "bakeme:latest",
+ mode == "release" ? "bakeme:latest" : "bakeme:dev"
]
- Si
modeesrelease, el nombre de etiqueta esbakeme:latest - Si
modeesdebug, el nombre de etiqueta esbakeme: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.
# 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.
TipLas 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: