Aller au contenu

Simplifiez vos Constructions d'Images avec bake

Mise à jour :

logo docker

Docker Bake est une fonctionnalité puissante et flexible intégrée à Docker Buildx, conçue pour simplifier et orchestrer la construction d’images Docker complexes. Contrairement à la commande docker build, qui est souvent linéaire et nécessite des scripts séparés pour gérer plusieurs builds, Docker Bake permet de définir des recettes de build dans un fichier de configuration unique.

Comprendre Docker Buildx

Docker Buildx est un outil avancé qui étend les capacités de la commande classique docker build. Il permet de créer des images pour différentes architectures, de gérer des multiples contextes, et d’utiliser des fonctionnalités avancées comme les caches de build. Buildx est conçu pour améliorer les workflows de build en apportant plus de flexibilité et de contrôle. Docker Bake, en tant qu’extension de Buildx, tire parti de ces fonctionnalités pour orchestrer des builds complexes via des fichiers de configuration.

Pour plus de détails sur Docker Buildx, vous pouvez consulter mon guide .

Différence entre Docker Compose et Docker Bake

Il est important de comprendre que Docker Compose et Docker Bake sont conçus pour des tâches fondamentalement différentes dans l’écosystème Docker. Docker Compose est un outil utilisé pour définir et gérer des applications multi-conteneurs. Il permet de décrire la manière dont plusieurs services interagissent et sont déployés ensemble dans un environnement orchestré.

En revanche, Docker Bake se concentre uniquement sur la construction d’images Docker. Il permet de centraliser les configurations de build dans un fichier unique, orchestrant ainsi des processus de build complexes de manière plus efficace. Par exemple, alors que Docker Compose vous permet de spécifier que vous avez besoin d’une base de données et d’un service web pour fonctionner ensemble, Docker Bake vous permet de définir comment ces services sont construits, en précisant les instructions pour créer les images nécessaires.

Installation et configuration de Docker Bake

Pour commencer à utiliser Docker Bake, il est nécessaire de configurer Docker Buildx, qui est le moteur sous-jacent de Bake. Docker Buildx est inclus dans Docker Desktop, mais il peut être activé manuellement si vous utilisez Docker en ligne de commande.

Un petit contrôle :

Terminal window
docker buildx ls

Si vous n’avez pas de builder, reportez-vous à ma documentation.

Structure et syntaxe des fichiers Bake

Les fichiers de configuration Bake définissent comment Docker Bake doit orchestrer vos builds. Par défaut, Docker Bake cherche le fichier de configuration en suivant un ordre précis : il commence par compose.yaml, compose.yml, et docker-compose.yml, avant de chercher des fichiers spécifiques à Bake comme docker-bake.json et docker-bake.hcl. Cette flexibilité permet d’utiliser des configurations existantes tout en tirant parti des fonctionnalités avancées de Bake.

Formats de fichiers supportés

Docker Bake prend en charge plusieurs formats de fichiers pour la configuration :

  • HCL : que nous utiliserons dans cette documentation
  • JSON
  • YAML

Exemple de fichier HCL pour Docker Bake :

group "default" {
targets = ["frontend", "backend"]
}
target "frontend" {
context = "./frontend"
dockerfile = "Dockerfile"
}
target "backend" {
context = "./backend"
dockerfile = "Dockerfile"
args = {
NODE_ENV = "production"
}
}

Ordre de recherche par défaut

Docker Bake utilise l’ordre suivant pour trouver automatiquement les fichiers de configuration :

  1. compose.yaml
  2. compose.yml
  3. docker-compose.yml
  4. docker-compose.yaml
  5. docker-bake.json
  6. docker-bake.override.json
  7. docker-bake.hcl
  8. docker-bake.override.hcl

Si vous souhaitez spécifier explicitement le fichier à utiliser, vous pouvez le faire avec l’option --file lors de l’exécution de la commande docker buildx bake :

Terminal window
docker buildx bake --file ./custom-bake.hcl

Cette flexibilité permet de structurer vos projets comme vous le souhaitez, tout en garantissant que Docker Bake trouve et utilise la bonne configuration pour orchestrer vos builds.

Définition des cibles de build

Les cibles de build dans Docker Bake permettent de structurer et d’organiser vos processus de construction en définissant des configurations spécifiques pour chaque image Docker à construire. Chaque cible peut inclure des éléments tels que le contexte, le Dockerfile, des tags pour les images, des arguments de build, et bien plus encore.

Voici un exemple de cibles dans un fichier HCL :

target "frontend" {
context = "./frontend"
dockerfile = "Dockerfile"
tags = ["myapp/frontend:latest"]
}
target "backend" {
context = "./backend"
dockerfile = "Dockerfile"
args = {
NODE_ENV = "production"
}
tags = ["myapp/backend:latest"]
}

Dans cet exemple, deux cibles distinctes sont définies pour la construction des images frontend et backend. La première cible construit une image pour le frontend à partir du répertoire ./frontend, en utilisant un fichier Dockerfile situé dans ce même répertoire, et lui assigne le tag myapp/frontend:latest. La seconde cible configure une image backend en utilisant le contexte ./backend, avec un argument de build NODE_ENV défini sur “production”, et assigne le tag myapp/backend:latest.

Une fois les cibles définies, vous pouvez les exécuter simultanément ou individuellement en utilisant Docker Bake. Cela permet d’optimiser le temps de build en parallèle, surtout lorsque les images n’ont pas de dépendances directes entre elles. Pour exécuter toutes les cibles d’un groupe, il suffit d’utiliser la commande suivante :

Terminal window
docker buildx bake

Cette commande va automatiquement repérer les cibles définies dans le fichier Bake et les exécuter en fonction de la configuration. Si vous souhaitez exécuter une cible spécifique, vous pouvez la préciser comme ceci :

Terminal window
docker buildx bake frontend

Docker Bake vous offre ainsi une grande flexibilité pour organiser et orchestrer des processus de build complexes en fonction des besoins spécifiques de votre projet.

Définition des groups

Les groupes vous permettent d’organiser plusieurs cibles en un seul ensemble. Cela permet d’exécuter plusieurs builds en parallèle ou de séquencer des builds en fonction des besoins du projet.

group "default" {
targets = ["frontend", "backend"]
}
target "frontend" {
context = "./frontend"
dockerfile = "Dockerfile"
tags = ["myapp/frontend:latest"]
}
target "backend" {
context = "./backend"
dockerfile = "Dockerfile"
args = {
NODE_ENV = "production"
}
tags = ["myapp/backend:latest"]
}

Héritage

Docker Bake supporte le concept d’héritage, permettant à une cible de build d’hériter des propriétés d’une ou plusieurs autres cibles. Ce mécanisme simplifie la gestion des configurations complexes en réutilisant et en étendant des blocs de construction existants. Par exemple, vous pouvez définir une cible parent avec des configurations communes, puis créer des cibles enfants qui héritent de ces propriétés tout en ajoutant ou en modifiant certains paramètres.

Exemple d’héritage :

target "base" {
context = "./"
dockerfile = "Dockerfile.base"
}
target "frontend" {
inherits = ["base"]
dockerfile = "Dockerfile.frontend"
tags = ["myapp/frontend:latest"]
}
target "backend" {
inherits = ["base"]
dockerfile = "Dockerfile.backend"
args = {
NODE_ENV = "production"
}
tags = ["myapp/backend:latest"]
}

Dans cet exemple, les cibles frontend et backend héritent toutes deux des propriétés de la cible base, ce qui permet de partager des configurations communes comme le contexte et certaines parties du Dockerfile. Cela réduit la duplication de code et rend les fichiers de configuration plus faciles à maintenir.

L’héritage est particulièrement utile dans des projets où plusieurs cibles partagent une grande partie de la même configuration, mais nécessitent des ajustements spécifiques. Il permet de centraliser les configurations communes tout en offrant la flexibilité nécessaire pour adapter chaque build aux besoins spécifiques du service ou de l’application.

Utilisation des variables

Docker Bake permet d’utiliser des variables pour rendre vos fichiers de configuration plus dynamiques et réutilisables. Les variables peuvent être définies dans le fichier de configuration Bake, passées via la ligne de commande, ou provenant de fichiers .env. Cela permet d’ajuster les builds en fonction de différents environnements ou configurations sans modifier directement le fichier Bake.

Exemple de fichier avec variables

variable "version" {
default = "1.0"
}
target "backend" {
context = "./backend"
tags = ["myapp/backend:${version}"]
}

Ici, la variable version est utilisée pour générer dynamiquement le tag de l’image.

Les variables peuvent être déclarées avec une valeur par défaut et peuvent être surchargées en ligne de commande :

Terminal window
docker buildx bake --set version=2.0

Expressions

Docker Bake prend en charge l’évaluation des expressions dans les fichiers HCL, ce qui permet d’effectuer des opérations arithmétiques, de définir des valeurs conditionnellement, et plus encore.

Opérations arithmétiques

Vous pouvez réaliser des calculs directement dans vos configurations. Par exemple, pour multiplier deux nombres :

sum = 7 * 6
target "default" {
args = {
answer = sum
}
}

En imprimant le fichier avec --print, Docker Bake évalue et affiche la valeur calculée :

"args": {
"answer": "42"
}

Opérateurs ternaires

Les opérateurs ternaires permettent de conditionner l’attribution de valeurs. Par exemple, ajouter un tag seulement si une variable n’est pas vide :

variable "TAG" {}
target "default" {
tags = [
"my-image:latest",
notequal("", TAG) ? "my-image:${TAG}" : "",
]
}

Si TAG est vide, seul le tag my-image:latest est appliqué.

Expressions avec des variables

Les expressions peuvent être combinées avec des variables pour conditionner les arguments de build ou effectuer des calculs. Par exemple :

variable "FOO" {
default = 3
}
variable "IS_FOO" {
default = true
}
target "app" {
args = {
v1 = FOO > 5 ? "higher" : "lower"
v2 = IS_FOO ? "yes" : "no"
}
}

Fonctions intégrées

Docker Bake propose une série de fonctions intégrées pour manipuler les données dans vos fichiers de configuration. Ces fonctions permettent d’effectuer des opérations comme la manipulation de chaînes, l’évaluation conditionnelle, et la gestion de listes. Par exemple, vous pouvez utiliser des fonctions pour concaténer des chaînes, transformer des variables, ou effectuer des conversions dynamiques.

Exemple de fonction :

target "example" {
tags = ["myapp/${upper(var.name)}:${replace(var.version, ".", "_")}"]
}

Ici, la fonction upper transforme var.name en majuscules, et replace substitue les points dans var.version par des underscores.

Docker Bake prend en charge plusieurs fonctions intégrées qui permettent de manipuler les chaînes de caractères, effectuer des opérations conditionnelles, et gérer des listes dans vos fichiers de configuration. Parmi les fonctions disponibles, on trouve :

  • upper et lower : convertissent une chaîne de caractères en majuscules ou minuscules.
  • replace : remplace une sous-chaîne par une autre.
  • contains : vérifie si une chaîne ou une liste contient un élément donné.
  • notequal : compare deux valeurs et retourne vrai si elles ne sont pas égales.
  • join : combine les éléments d’une liste en une seule chaîne.

Pour une liste complète et des exemples, consultez la documentation officielle.

Matrices de build

Docker Bake permet de définir des matrices de build, ce qui vous permet de construire plusieurs variantes d’une image Docker en une seule commande. Les matrices combinent plusieurs variables pour générer différentes configurations de build, comme des versions spécifiques de l’application ou des architectures cibles.

Exemple de matrice :

target "app" {
context = "./app"
dockerfile = "Dockerfile"
args = {
NODE_ENV = ["development", "production"]
}
platforms = ["linux/amd64", "linux/arm64"]
}

Cela construit les images pour chaque combinaison d’environnement et de plateforme.

Gestion des contextes

Docker Bake permet de spécifier des contextes de build pour définir les sources de vos builds, qu’il s’agisse de répertoires locaux, de référentiels Git, ou de fichiers tar. Un contexte de build fournit les fichiers et dossiers nécessaires pour construire l’image Docker. Vous pouvez également utiliser plusieurs contextes dans un même fichier Bake, permettant une flexibilité accrue dans la gestion des sources de vos builds.

target "multi-context" {
contexts = {
frontend = "./frontend"
backend = "./backend"
assets = "git://github.com/user/assets-repo.git"
}
dockerfile = "Dockerfile"
}

Mise en oeuvre

Dans ce tutoriel, nous allons créer un projet simple en utilisant Docker Bake avec un fichier HCL. Nous allons enrichir ce fichier progressivement tout en utilisant des concepts comme les cibles, les variables, les expressions, l’héritage, les contextes, les matrices de build.

Structure du projet

Voici la structure de notre projet :

  • docker-bake.hcl
  • Répertoirefrontend/
    • Dockerfile
    • index.js
  • Répertoirebackend/
    • Dockerfile
    • app.py
    • requirements.txt

Les fichiers

Pour le frontend

FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "index.js"]

Pour le backend

FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Étapes de la configuration Docker Bake

Dans un premier temps, nous allons créer les deux target pour le backend et le frontend :

group "default" {
targets = [
"backend",
"frontend"
]
}
target "backend" {
context = "./backend"
dockerfile = "./Dockerfile"
tags = ["backend:latest"]
}
target "frontend" {
context = "./frontend"
dockerfile = "./Dockerfile"
tags = ["frontend:latest"]
}

On créé le groupe default contenant les deux targets backend et frontend.

Validons le contenu du fichier avec la commande suivante :

Terminal window
docker buildx bake --print
[+] Building 0.0s (1/1) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading docker-bake.hcl 316B / 316B 0.0s
{
"group": {
"default": {
"targets": [
"backend",
"frontend"
]
}
},
"target": {
"backend": {
"context": "backend",
"dockerfile": "./Dockerfile",
"tags": [
"backend:latest"
]
},
"frontend": {
"context": "frontend",
"dockerfile": "./Dockerfile",
"tags": [
"frontend:latest"
]
}
}
}

Étape 2 : Ajout de variables pour la flexibilité

En bon DevOps, nous n’utilisons pas les tags latest. Remplaçons-les par des versions dans des variables

variable "frontend_version" {
default = "1.0.1"
}
variable "backend_version" {
default = "1.0.17"
}
group "default" {
targets = [
"backend",
"frontend"
]
}
target "backend" {
context = "./backend"
dockerfile = "./Dockerfile"
tags = ["backend:${frontend_version}"]
}
target "frontend" {
context = "./frontend"
dockerfile = "./Dockerfile"
tags = ["frontend:${frontend_version}"]
}

On relance la commande de check :

Terminal window
docker buildx bake --print
[+] Building 0.2s (1/1) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading docker-bake.hcl 530B / 530B 0.0s
{
"group": {
"default": {
"targets": [
"backend",
"frontend"
]
}
},
"target": {
"backend": {
"context": "backend",
"dockerfile": "./Dockerfile",
"tags": [
"backend:1.0.17"
],
"output": [
"type=tar,dest=backend.tar.gz"
]
},
"frontend": {
"context": "frontend",
"dockerfile": "./Dockerfile",
"tags": [
"frontend:1.0.1"
],
"output": [
"type=tar,dest=frontend.tar.gz"
]
}
}
}

On voit que les tags ont pris les bonnes valeurs. Tentons de construire nos images :

Terminal window
docker buildx bake
[+] Building 1.1s (19/19) FINISHED docker:default
=> [internal] load local bake definitions 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:552e6e90efdca848652f37bd4eb58dd9a74aeea5fd2371816b124637d872b348 0.0s
=> => naming to docker.io/library/backend:1.0.17 0.0s
=> CACHED [frontend 2/3] WORKDIR /app 0.0s
=> CACHED [frontend 3/3] COPY . . 0.0s
=> [frontend] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:926f65eb76fb9d369c049d90462ba5b9e713617c64cc1895ff2d7074aba8e782 0.0s
=> => naming to docker.io/library/frontend:1.0.1 0.0s

Le build s’est bien passé en parallèle. On vérifie les images :

Terminal window
REPOSITORY TAG IMAGE ID CREATED SIZE
frontend 1.0.1 926f65eb76fb 3 minutes ago 1.09GB
backend 1.0.17 552e6e90efdc 4 minutes ago 146MB```
#### Utilisation des matrices
Nous allons utiliser la matrice pour produire des images multi-plateformes. Pour
cela, il faut ajouter un builder utilisant le driver `docker-container` :
```bash
docker buildx create --driver docker-container --bootstrap --name mybuilder
[+] Building 32.0s (1/1) FINISHED
=> [internal] booting buildkit 32.0s
=> => pulling image moby/buildkit:buildx-stable-1 31.2s
=> => creating container buildx_buildkit_mybuilder0 0.8s
mybuilder

Définissons-le comme builder par défaut :

Terminal window
docker buildx use mybuyilder

Ajouter dans les targets les plateformes cibles :

target "backend" {
context = "./backend"
dockerfile = "./Dockerfile"
tags = ["backend:${frontend_version}"]
platforms = ["linux/amd64", "linux/arm64"]
output = ["type=tar,dest=frontend.tar.gz"]
}
target "frontend" {
context = "./frontend"
dockerfile = "./Dockerfile"
tags = ["frontend:${frontend_version}"]
platforms = ["linux/amd64", "linux/arm64"]
output = ["type=tar,dest=frontend.tar.gz"]
}

Lançons le build :

Terminal window
docker buildx bake
...
=> [backend linux/arm64 2/5] WORKDIR /app 59.9s
=> [backend linux/amd64 2/5] WORKDIR /app 31.5s
=> [backend linux/arm64 3/5] COPY requirements.txt . 47.1s
=> [backend linux/amd64 3/5] COPY requirements.txt . 163.3s
=> [backend linux/arm64 4/5] RUN pip install -r requirements.txt 392.1s
=> [frontend linux/arm64 2/3] WORKDIR /app 52.6s
=> [frontend linux/amd64 2/3] WORKDIR /app 49.0s
=> [backend linux/amd64 4/5] RUN pip install -r requirements.txt 240.5s
=> [frontend linux/amd64 3/3] COPY . . 1.0s
=> [frontend linux/arm64 3/3] COPY . . 1.0s
=> [frontend] exporting to client tarball 206.7s
=> => sending tarball 206.7s
=> [backend linux/arm64 5/5] COPY . . 1.6s
=> [backend linux/amd64 5/5] COPY . . 1.6s
=> [backend] exporting to client tarball 29.7s
=> => sending tarball

On retrouve nos deux fichiers tar.gz dans le dossier où la commande a été lançé.

Ce petit tutoriel montre comment structurer un projet Docker avec Docker Bake, permettant des builds flexibles et optimisés pour différents environnements et plateformes.

Pour approfondir, consultez la documentation Docker Bake.

Conclusion

En utilisant Docker Bake, nous avons pu orchestrer de manière efficace les builds d’une application multi-service composée d’un frontend Node.js et d’un backend Flask. Grâce à la simplicité et à la flexibilité offertes par Docker Bake, nous avons pu gérer les configurations, optimiser les builds, et centraliser la gestion des services dans un fichier unique. Cet outil s’avère indispensable pour les projets nécessitant des builds complexes et des déploiements automatisés, tout en conservant une approche déclarative et cohérente.

Docker Bake facilite ainsi le workflow DevOps, permettant de se concentrer davantage sur le développement et l’optimisation des applications, tout en simplifiant la gestion des builds et des déploiements. Avec une configuration bien pensée, vous pouvez aisément gérer les besoins de votre projet, qu’il s’agisse de services simples ou d’environnements complexes à plusieurs composants.

Pour aller plus loin et explorer d’autres cas d’utilisation de Docker Bake, vous pouvez consulter la documentation officielle de Docker Bake.