Aller au contenu principal

Maitriser BuildKit

logo docker

Introduction

Depuis la version 23.0 de Docker, BuildKit est devenu le moteur de création d'images de conteneurs par défaut en remplacement de l'ancien qui devenait obsolète face à ses concurrents que sont Buildah, Kaniko par exemple. Aujourd'hui, je vais parler de certaines nouvelles fonctionnalités qui permettent d'utiliser différents contextes de construction d'images.

Petit tour des fonctionnalités de Buildkit

BuildKit est pour moi une petite révolution dans le domaine des outils de constructions d'image que j'ai manquée. Je m'étais amusé à l'utiliser pour générer des images en parallèle une image à destination de plusieurs plateformes, mais sans en saisir tout le potentiel des nouvelles fonctionnalités qu'il intégrait.

En fait BuildKit est un produit à part entière. Il possède une architecture permettant de supporter d'autres langages de définition d'images, autre que les Dockerfile, mais aussi plusieurs formats de sortie des images. Il peut être utilisé via Docker Desktop ou Docker Engine, déployé sur des machines distantes, mais aussi des clusters Kubernetes.

Pour améliorer les temps de constructions, il fait appel à un DAG qui permet :

  • De paralléliser la construction des étapes indépendantes.
  • De détecter et de bypasser des étapes inutiles
  • D'améliorer la copie des fichiers depuis le contexte local
  • D'utiliser des caches dans des volumes

L'intégration de Buildkit dans Docker Engine

Le sujet est complexe à expliquer, car la documentation est difficile à comprendre. Elle est éclatée un peu partout et pas forcément à jour.

Il faut bien comprendre que BuildKit est un moteur de construction d'images qui est intégré dans Docker Engine et Docker Desktop. L'intégration dans Docker Engine se fait via le plugin buildx qui normalement est installé avec. Vérifions :

dpkg --get-selections|grep docker
docker-buildx-plugin install
docker-ce install
docker-ce-cli install
docker-ce-rootless-extras install
docker-compose-plugin install

Le plugin Buildx est défini par défaut, mais pourrait être remplacé par un autre.

Buildx est très souple, car il permet d'utiliser différents drivers. Chaque pilote définit comment et où la construction de l'image se fait et possède ses propres fonctionnalités.

Parmi les drivers actuellement supportés, on trouve les suivants :

  • Le driver docker le driver par défaut qui utilise Docker installé localement.
  • Le driver docker-container qui permet la création d'un environnement BuildKit personnalisable dans un conteneur Docker.
  • Le driver kubernetes qui permet de lancer des constructions sur des clusters kubernetes.
  • Le driver remote qui permet de lancer des constructions sur des machines distantes ou BuildKit est installé en tant que démon.

On voit que tout est pensé pour le rendre très souple et pour pouvoir déporter les constructions d'images de containers un peu partout. Dans ce billet, je vais me limiter à l'utiliser aux drivers docker et docker-container, mais je prévois d'écrire des billets sur les deux autres drivers.

Le driver docker

Le driver Docker ne possède pas de paramètres pour le moment. C'est celui qui est invoqué avec la commande docker build. Il ne permet par exemple de construire d'images pour d'autres plateformes.

Le driver docker-container

Le driver docker-container possède les paramètres suivants :

  • image=<IMAGE> qui permet de définir l'image du conteneur à utiliser pour exécuter BuildKit.
  • network=<NETMODE> qui permet de définir le mode réseau pour exécuter le conteneur BuildKit.
  • cgroup-parent=<CGROUP> qui permet de définir le cgroup parent du conteneur BuildKit si docker utilise le pilote cgroupfs. La valeur par défaut est /docker/buildx.

Dans ce billet, nous n'utiliserons aucune de ces options.

Gestion des instances

Les instances de constructeur sont des environnements isolés dans lesquels des builds peuvent être lancés.

Lister les instances

Pour obtenir la liste des instances disponibles, on utilise la commande ls :

docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default * docker default default running v0.11.6+616c3f613b54 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386

On ne retrouve que celui du driver docker par défaut.

Inspection d'une instance

Pour obtenir la liste complète des paramètres actuels, on fait appel à la commande inspect.

docker buildx inspect default
Name: default
Driver: docker
Last Activity: 2023-10-25 09:03:27 +0000 UTC

Nodes:
Name: default
Endpoint: default
Status: running
Buildkit: v0.11.6+616c3f613b54
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
Labels:
org.mobyproject.buildkit.worker.moby.host-gateway-ip: 172.17.0.1

Création d'une instance

La commande create permet de créer de nouvelles instances de constructeur. Voyons comment créer une nouvelle instance utilisant le driver docker-container.

docker buildx create --driver docker-container --bootstrap --name mybuilder
[+] Building 5.8s (1/1) FINISHED
=> [internal] booting buildkit 5.8s
=> => pulling image moby/buildkit:buildx-stable-1 5.2s
=> => creating container buildx_buildkit_mybuilder0 0.5s
mybuilder

L'option --bootstrap permet de démarrer l'instance après sa création.

Faisons quelques vérifications :

docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d831723dd9e0 moby/buildkit:buildx-stable-1 "buildkitd" 11 minutes ago Up 11 minutes buildx_buildkit_mybuilder0

On retrouve une image de BuildKit fournie par le projet moby sur lequel tourne le démon buildkitd.

docker build ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-container
mybuilder0 unix:///var/run/docker.sock running v0.12.3 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
default docker
default default running v0.11.6+616c3f613b54 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386

Construction d'une image utilisant le driver docker-container

On prend un simple Dockerfile :

FROM alpine:3.18
CMD ["/bin/echo", "Build with BuildKit!"]

Lors du build, il faut indiquer la sortie, car lors de la création de l'instance, elle n'a pas été indiqué. On a à notre disposition deux options :

  • --load pour la charger dans Docker
  • --push pour la pousser directement dans une registry

On utilisera ici l'option --load :

docker buildx build . --tag my-image:0.1 --load
[+] Building 0.9s (6/6) FINISHED docker-container:mybuilder
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 95B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18 0.9s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [1/1] FROM docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> => resolve docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> exporting to docker image format 0.0s
=> => exporting layers 0.0s
=> => exporting manifest sha256:ffee8b73b5c65b7371e6a5cd2e8af178253eff5ce9a35ae7afe6785b934817ac 0.0s
=> => exporting config sha256:cd081914e1d135e58bc9ad0aa40a874050ac7f338b94599a7a8c367ba88c685d 0.0s
=> => sending tarball 0.0s
=> importing to docker 0.0s
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-image 0.1 cd081914e1d1 3 weeks ago 7.34MB

Notre image a bien été créé !

Le multi-plateforme

Nous allons créer une seconde instance prenant en charge des plateformes autres celles de l'hôte.

docker buildx create --use --bootstrap \
--name mybuilder2 \
--driver docker-container \
> --platform linux/arm64,linux/arm/v8
[+] Building 0.0s (0/1)
[+] Building 3.1s (1/1) FINISHED
=> [internal] booting buildkit 3.1s
=> => pulling image moby/buildkit:buildx-stable-1 2.6s
=> => creating container buildx_buildkit_mybuilder20 0.5s
mybuilder2

Vérifions que nous avons bien notre nouvelle instance :

docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder docker-container
mybuilder0 unix:///var/run/docker.sock running v0.12.3 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
mybuilder2 * docker-container
mybuilder20 unix:///var/run/docker.sock running v0.12.3 linux/arm64*, linux/arm/v8*, linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
default docker
default default running v0.11.6+616c3f613b54 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386 ```

On voit bien que nos plateformes s'ajoutent à celle de notre hôte.

On tente le build en indiquant la plateforme arm/v8 :

docker buildx build . --tag my-image:0.1 --load --platform linux/arm/v8
[+] Building 7.1s (7/7) FINISHED docker-container:mybuilder2
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 95B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18 4.2s
=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/1] FROM docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 2.8s
=> => resolve docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> => sha256:622a0779436eb93ceea635e910268f867c2eba47d4f62f0bd45f0bd165af3572 2.90MB / 2.90MB 2.8s
=> exporting to docker image format 2.8s
=> => exporting layers 0.0s
=> => exporting manifest sha256:56312a9ccaaea5c7f13803bee97f4eea883282ba10cb6497abb475574a2ca4b3 0.0s
=> => exporting config sha256:2544f3c45a5f12c7b6078d0896b52fcbb61404e971f6e1c25aef659b900e4073 0.0s
=> => sending tarball 0.0s
=> importing to docker

On inspecte l'image :

docker image inspect my-image:0.1
[
{
"Id": "sha256:2544f3c45a5f12c7b6078d0896b52fcbb61404e971f6e1c25aef659b900e4073",
"RepoTags": [
"my-image:0.1"
],
"RepoDigests": [],
"Parent": "",
"Comment": "buildkit.dockerfile.v0",
"Created": "2023-09-28T20:59:24.681335468Z",
"Container": "",
"ContainerConfig": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": null,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"DockerVersion": "",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/echo",
"Build with BuildKit!"
],
"ArgsEscaped": true,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "arm",
"Variant": "v8",
"Os": "linux",
"Size": 4694566,
"VirtualSize": 4694566,
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/d2d07f0bf6c88f3dc68bc8f23d58ead2b3c873b1af449fe97ffcbec2e418e806/merged",
"UpperDir": "/var/lib/docker/overlay2/d2d07f0bf6c88f3dc68bc8f23d58ead2b3c873b1af449fe97ffcbec2e418e806/diff",
"WorkDir": "/var/lib/docker/overlay2/d2d07f0bf6c88f3dc68bc8f23d58ead2b3c873b1af449fe97ffcbec2e418e806/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:315bd5088587f904ce90b0801a0c8db45648746faf106758d6946f4ef96ae5d0"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]

On a bien une image pour les processeurs arm/v8. Cela simplifie énormément le process à mon précédent test.

Définition de l'instance par défaut

La commande use permet de définir l'instance à utiliser par défaut.

docker buildx use default

Lors de la création d'une instance, il est possible d'utiliser le paramètre --use pour basculer directement sur celui-ci.

Arrêt d'une instance

L'arrêt d'une instance se fait avec la commande stop :

docker buildx stop mybuilder2

Vérifions le resultat :

docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder docker-container
mybuilder0 unix:///var/run/docker.sock running v0.12.3 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
mybuilder2 * docker-container
mybuilder20 unix:///var/run/docker.sock stopped linux/arm64*, linux/arm/v8*
default docker
default default running v0.11.6+616c3f613b54 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386

Il n'y a pas de commande start car il suffit de relancer un build pour que l'instance démarre.

Destruction des instances

La commande rm permet de détruire une instance de construction.

Faire du ménage

Buuildkit utilise des ressources et il faut pouvoir faire de temps en temps un peu le ménage. Pour cela, nous avons deux commandes : du pour obtenir des informations sur l'espace disque occupé et prune pour le nettoyage.

docker buildx du
ID RECLAIMABLE SIZE LAST ACCESSED
g1m8rrh7yz00ud5xon5zq2a7b true 2.9MB About a minute ago
k8pfdeh1dna0kwrkvil9brtou* true 8.192kB About a minute ago
qvz1azsg8rxfofd0qzcha6r50* true 4.096kB About a minute ago
Reclaimable: 2.912MB
Total: 2.912MB

docker buildx prune
WARNING! This will remove all dangling build cache. Are you sure you want to continue? [y/N] y
ID RECLAIMABLE SIZE LAST ACCESSED
g1m8rrh7yz00ud5xon5zq2a7b true 2.9MB 4 minutes ago
k8pfdeh1dna0kwrkvil9brtou* true 8.192kB 4 minutes ago
qvz1azsg8rxfofd0qzcha6r50* true 4.096kB 4 minutes ago
Total: 2.912MB

Dans mon test vu que mon image est des plus simples cela ne change rien. Mais si on construit des images plus complexes alors là cela change tout.

Conclusion

En conclusion, BuildKit offre aux administrateurs systèmes et aux développeurs une solution plus rapide, plus sûre et plus efficace pour la construction d'images Docker. Grâce à ses fonctionnalités avancées telles que la mise en cache, la parallélisation des builds, et la gestion optimisée des dépendances, BuildKit permet non seulement d'économiser du temps et des ressources, mais contribue également à renforcer la sécurité des images Docker.

L'intégration de BuildKit dans vos workflows DevOps peut significativement améliorer la vitesse de développement et de déploiement, tout en assurant la conformité aux normes de sécurité. Avec les bonnes pratiques et une configuration appropriée, BuildKit ouvre la porte à des builds plus robustes et à une meilleure gestion des containers.

Je vous encourage vivement à adopter BuildKit dans vos projets, à expérimenter avec ses fonctionnalités avancées, et à partager vos propres expériences et astuces avec la communauté. Ensemble, continuons à pousser les limites de l'efficacité et de la sécurité dans le développement de logiciels.