forked from quentin/quentin.dufour.io
127 lines
7.4 KiB
Markdown
127 lines
7.4 KiB
Markdown
---
|
|
layout: post
|
|
title: Construire et publier des conteneurs sans daemon Docker
|
|
date: 2023-04-11T10:51:40.008+02:00
|
|
status: draft
|
|
sitemap: true
|
|
category: developpement
|
|
description: Construire et publier des conteneurs légers et multi-plateformes
|
|
depuis une CI/CD implique plusieurs défis à relever quand on a pas de daemon
|
|
Docker à disposition, je vous partage mes réflexions.
|
|
---
|
|
J'ai pas mal travaillé sur la CI/CD de [Garage](https://garagehq.deuxfleurs.fr/), et force est de constater qu'on a rencontré un nombre incroyable de problèmes. Entre autre, on a noté que les builds Rust sans cache sont trop lents par rapport à nos attentes, qu'il n'y avait pas de solution légère pour gérer les artefacts binaires et enfin que construire un conteneur quand on a un CI/CD à base de Docker, ça n'était pas possible car on n'avait pas accès au daemon docker ni la possibilité de faire du “docker in docker” de manière à peu près sécurisé.
|
|
|
|
Si la question du cache et des artefacts binaires est passionnante, nous allons la garder pour un autre billet de blog, et nous focaliser sur **comment construire des conteneurs légers, multi-plateforme et les publier** dans ce billet. Si vous ne voyez pas ce que j'entends par registre statique, allez donc [jeter un coup d'oeil à mon précédent billet !](https://quentin.dufour.io/blog/2023-04-06/un-registre-statique-docker-avec-garage/)
|
|
|
|
Alors maintenant qu'on a notre périmètre, décortiquons le:
|
|
|
|
- **léger** : c'est à dire qui embarque le strict minimum. Bien souvent, on peut se contenter d'un binaire statique.
|
|
- **multi-plateforme :** un seul tag d'image permettra à des gens sur ARM comme sur X86_64 d'utiliser votre logiciel
|
|
- **publier** : on publier les conteneurs sur un registre, ici nous verrons comment faire sur le docker hub mais aussi sur notre registre statique à base de Garage
|
|
|
|
_À noter qu'il y a un dernier point qui ne sera pas abordé dans ce billet qui sera sans aucun doute beaucoup trop long de toute manière, c'est comment gérer la garbage collection de nos artifacts._
|
|
|
|
## Une build file avec Nix Flake
|
|
|
|
Pour ce billet, on va prendre comme un exemple un programme en go que j'ai écrit, Albatros, ma propre CI/CD (ça devient déjà meta). L'avantage de prendre comme exemple un programme en Go, c'est que ça se cross compile facilement. Voilà un extrait du fichier `flake.nix` de notre projet :
|
|
|
|
```hcl
|
|
# declare the go module of this package, allow for cross compilation
|
|
gopkg = arch: (pkgs.buildGoModule rec {
|
|
pname = "albatros-go-module";
|
|
version = "0.9";
|
|
CGO_ENABLED = 0;
|
|
# ... skip
|
|
}).overrideAttrs (old: old // { GOOS = "linux"; GOARCH = arch; });
|
|
|
|
# logic to build static binaries
|
|
albatrosStaticBin = #... extract the binary from gopkg (skipped here)
|
|
|
|
# logic to build docker containers
|
|
docker = (staticBin: arch: pkgs.dockerTools.buildImage {
|
|
name = "dxflrs/albatros";
|
|
architecture = arch;
|
|
config = {
|
|
Cmd = [ "${staticBin}" ];
|
|
};
|
|
});
|
|
|
|
# map nixos/llvm arch to golang arch
|
|
archmap = {
|
|
"aarch64-linux" = "arm64";
|
|
"x86_64-linux" = "amd64";
|
|
"i686-linux" = "386";
|
|
"armv6l-linux" = "arm";
|
|
};
|
|
|
|
# generate packages for each architecture
|
|
packages = builtins.mapAttrs (name: value: {
|
|
docker.albatros = (docker (albatrosStaticBin value) value);
|
|
# other targets (skipped)...
|
|
}) archmap;
|
|
```
|
|
|
|
_On peut consulter le fichier en entier_ [_sur la forge_](https://git.deuxfleurs.fr/quentin/albatros/src/commit/d9facbb79c4551d90359c46b9f5d485c1503253a/flake.nix) _d'Albatros_.
|
|
|
|
Ce fichier est relativement simple à lire une fois qu'on sait comment l'aborder.
|
|
|
|
En fait on construit par rafinement successif. Le premier bloc consiste en une fonction qui permet de compiler un module Go à partir de la recette fournie par la bibliothèque standard NixOS. Je dis bien une fonction, car ce bloc prend en paramètre `arch` qui contient l'architecture cible de notre module. Ainsi, si on lui passe `arm64` on aura un binaire qui fonctionne sur les processeurs ARM 64 bits, si on passe `386`, on aura un binaire pour les vieux PC x86 32 bits, etc.
|
|
|
|
Dans les blocs suivants, on raffine donc ce premier module. On va d'abord avoir une fonction qui va extraire le binaire statique du module généré par Go, ensuite une fonction Docker qui va mettre ce binaire statique dans un conteneur.
|
|
|
|
Enfin, une fois notre logique définie, on va déclarer quelles architectures on choisit de supporter, là j'en ai choisi 4. On va donc faire une boucle (`mapAttrs`) pour générer le conteneur Docker qui va bien pour chaque architecture.
|
|
|
|
## Créer les artefacts avec nix build
|
|
|
|
On peut ensuite créer nos différentes archives Docker, en précisant le chemin de sortie pour s'y retrouver :
|
|
|
|
```bash
|
|
nix build .#packages.x86_64-linux.docker.albatros -o albatros.amd64.tar.gz
|
|
nix build .#packages.armv6l-linux.docker.albatros -o albatros.arm.tar.gz
|
|
nix build .#packages.aarch64-linux.docker.albatros -o albatros.arm64.tar.gz
|
|
nix build .#packages.i686-linux.docker.albatros -o albatros.386.tar.gz
|
|
```
|
|
|
|
## Construire notre image multi-arch
|
|
|
|
Dans le monde des conteneurs, une image multiarch est juste une indirection, un fichier qui contient une liste de manifest avec des tags pour leur OS et leur architecture. Il faut donc créer un fichier qui liste le manifest de chacune de nos 4 images.
|
|
|
|
_Problème : aujourd'hui il n'y a pas vraiment d'outils clé en main. Typiquement,_ [_une issue sur skopeo_](https://github.com/containers/skopeo/issues/1136) _traine depuis 3 ans maintenant (2020) sans qu'elle n'ait jamais été résolue. On va essayer de bidouiller un truc de notre côté._
|
|
|
|
On va extraire chacun de ces fichiers sous forme de dossier avec skopeo.
|
|
|
|
_Il y a deux façons de représenter des images de conteneur sous forme de dossier avec skopeo : via le transport_ `_dir_` _et le transport_ `_oci_`_. Le premier est un format interne non spécifié à skopeo. Le second est standardisé et a donc_ [_une spécification en bonne et due forme_](https://github.com/opencontainers/image-spec/blob/main/image-layout.md)_. Nous, on va préférer utiliser un standard pour éviter les mauvaises surprises à l'avenir (changement de format, abandon du support, interopérabilité, etc.)._
|
|
|
|
```bash
|
|
mkdir -p /tmp/oci
|
|
skopeo --insecure-policy copy docker-archive:albatros.amd64.tar.gz oci:/tmp/oci/amd64
|
|
skopeo --insecure-policy copy docker-archive:albatros.arm64.tar.gz oci:/tmp/oci/arm64
|
|
skopeo --insecure-policy copy docker-archive:albatros.arm.tar.gz oci:/tmp/oci/arm
|
|
skopeo --insecure-policy copy docker-archive:albatros.386.tar.gz oci:/tmp/oci/386
|
|
```
|
|
|
|
On va ensuite construire à la main le dossier multiarch :
|
|
|
|
```bash
|
|
mkdir -p /tmp/oci/multi
|
|
|
|
# on copie juste le fichier qui déclare la version de la spec "directory" de OCI
|
|
cp /tmp/oci/amd64/oci-layout /tmp/oci/multi/
|
|
|
|
# on copie les blobs
|
|
mkdir -p /tmp/oci/multi/blobs/sha256/
|
|
cp -r /tmp/oci/{386,arm,arm64,amd64}/blobs/sha256/* /tmp/oci/multi/blobs/sha256/
|
|
|
|
# on recupere les blobs des manifests de chacune de nos images :
|
|
for a in amd64 arm arm64 386; do
|
|
F=$(jq -r '.manifests[0].digest' /tmp/oci/$a/index.json|grep -Po '[0-9a-f]+$');
|
|
echo $a $'\t' $(stat -c %s /tmp/oci/$a/blobs/sha256/$F) $'\t' $F
|
|
done
|
|
# amd64 405 bc16dc0ab502dedbce06f16f51d46f7027271e20a378c7f2821bf5e000197523
|
|
# arm 405 6d9017b7292a7922b410db767a9418accc700d529c9202d3c0155de10dc330cf
|
|
# arm64 405 18a092e389b2ee93d8170fed24a39bf1af38d2d05d7b6ee107d3af50b17f7dd0
|
|
# 386 405 d88fd35b9c83baa3c8d61df4dc99a7c35346e493a2981d67d2d458652a5cac8f
|
|
|
|
```
|
|
|
|
Maintenant il ne nous reste plus qu'à créer un manifeste depuis les informations qu'on a collecté !
|