Intégration d'image Docker via Yocto

29 juillet 2024 ― Pierre-Loup GOSSE

Résumé ― L'intégration d'image Docker dans une distribution Yocto n'est pas proposée par le layer meta-virtualization qui se limite à la construction d'image OCI. Cet article présente le layer meta-dockin, solution simple d'intégration s'alignant à la philosophie de Yocto mais qui présente des désavantages face à une approche moins conventionnelle. Nous rentrerons dans les détails d'implémentation du layer meta-dockin, avec l'ajout d'un nouveau fetcher et d'une classe bitbake.

Intégration dans Yocto

L'intégration d'image Docker dans une distribution Yocto consiste à fournir au runtime un data-root opérationnel contenant les images Docker, c'est-à-dire sans nécessité de les télécharger (docker pull). Dans cet article nous abordons uniquement l'intégration d'image Docker déjà construite et disponible sur un registry public ou privée.

Nous allons dans cette section voir des limites du layer meta-virtualization et présenter les deux approches répondant au besoin.

Les limites de meta-virtualization

On retrouve dans le layer meta-virtualization la recette docker-moby_git.bb pour installer Docker-CE (moby + CLI docker). Le layer offre aussi la possibilité de générer des images OCI à partir de recette bitbake. La recette image app-container.bb dans le dossier recipes-demo/images/ en est un exemple : celle-ci hérite de la classe bitbake image-oci.bbclass pour construire une image OCI à partir du rootfs généré via l'ajout d'un fstype "oci" (dont l'IMAGE_CMD est définit dans le fichier image-oci-umoci.inc).

Cependant, le layer ne supporte pas aujourd'hui l'intégration des images conteneurs dans le rootfs. C'est un choix expliqué par Bruce Ashfield ― le mainteneur derrière meta-virtualization ― dans la discussion Importing Docker Images* de la mailing list ainsi que dans sa conférence Building and deploying containers with meta-virtualization: now & in the future.

L'intégration reste donc pour l'instant à la charge du développeur.

Intégration au build time

L'idée de cette méthode est de créer une recette bitbake destinée à pull les images dans un data-root local à la recette et d'installer celui-ci dans le rootfs. Pour ce faire, une tâche lance en arrière-plan le daemon Docker dockerd avec l'option --data-root pointant vers un dossier du WORKDIR de la recette. La commande docker pull peut ensuite être exécutée pour chaque image, après quoi le daemon est éteint.

Bien que l'implémentation de cette méthode soit simple, et qu'elle permet d'avoir un data-root déjà prêt au runtime, elle présente des inconvénients :

  • Le daemon requière les droits priviligiés, l'usage de sudo étant une mauvaise pratique dans Yocto et la configuration d'un Docker rootless peut compliquer les choses.
  • Le build ne peut pas fonctionner avec un autre daemon Docker en parallèle sur une même machine.
  • Dans le cas d'une exécution depuis une image Docker, le daemon impliquera des droits supplémentaires pour utiliser mount et unshare et présentera des erreurs de droits mais contournable en autorisant l'accès au network sur la tâche (non recommandé).
  • Cette méthode ne rentre pas dans la philisophie de Yocto.

Le layer meta-embedded-containers est une implémentation proof-of-concept de cette solution.

Intégration en deux temps

Cette seconde méthode consiste d'abord à récupérer les images Docker au format d'archive et de les intégrer dans le rootfs. Ensuite, au premier boot, de charger les archives Docker dans le data-root avec la commande docker load (via un service systemd par exemple).

Moins contraignant au build time, cette méthode nécessite cependant que le data-root soit persistant pour éviter le chargement des archives à chaque démarrage. Cette méthode est d'ailleurs mise en pratique dans la conférence OCI/Docker containers with meta-virtualization and OE/the Yocto Project.

Meta-dockin

Il y a plusieurs approche pour implémenter la solution à deux temps, le layer meta-dockin que j'ai conçu au sein d'un projet client en est un exemple. Le besoin était d'installer une ou plusieurs images Docker avec d'autres éléments, tel qu'un fichier Docker compose. Pour ce faire, le layer offre la possibilité d'intégrer des images Docker à partir de la variable SRC_URI d'une recette.

Voici un aperçu d'une recette permettant d'installer l'image Docker hello-world :

inherit dockin

SRC_URI += "docker://hello-world"

Le layer se compose de trois éléments :

  • D'un fetcher docker permettant de télécharger les archives des images Docker depuis la variable SRC_URI.
  • D'une classe bitbake dockin permettant de compresser et d'installer les archives dans le rootfs.
  • D'un service systemd dockin-preload.service exécuté au runtime pour charger les archives dans le data-root.

Nous allons dans les sections suivantes développer ces différents éléments.

Module bitbake fetch

La tâche do_fetch utilise le module fetch de bitbake pour télécharger les sources transmisent par la variable SRC_URI. Des sous-modules, dit fetchers, permettent de supporter le téléchargement de plusieurs types d'URL. Par exemple, un fetcher git pour les URLs git://, un fetcher wget pour les URLs http:// et https://, etc.

Le module fetch est présent le dossier bitbake/lib/bb/fetch2 du layer meta-poky. Le fichier __init__.py est assez conséquent, il contient les classes et la logique du module. On peut retenir que la classe FetchMethod sert de base pour l'implémentation des fetchers, que la classe FetchData représente les données associées à une URL, dont sa FetchMethod, et que la classe Fetch, le point d'entrée du module, créer les objets FetchData pour chaque URL et exécute les actions via leur FetchMethod.

Les autres fichiers python sont les implémentations des fetchers, c'est-à-dire des classes héritant de FetchMethod et implémentant au moins les méthodes suivantes :

  • supports : indique si le fetcher supporte le type de l'URL.
  • urldata_init : pour compléter les données du FetchData pouvant servir lors du téléchargement.
  • download : implémente le téléchargement de la ressource.
  • checkstatus : implémente la vérification du statut de l'URL.

La tâche do_fetch créée simplement un objet Fetch et exécute l'action de téléchargement :

def do_fetch(d):
    src_uri = (d.getVar('SRC_URI') or "").split()
    if not src_uri:
        return

    try:
        fetcher = bb.fetch2.Fetch(src_uri, d)
        fetcher.download()
    except bb.fetch2.BBFetchException as e:
        bb.fatal("Bitbake Fetcher Error: " + repr(e))

Créer un nouveau fetcher

Malgré le manque de documentation à ce sujet, la création d'un nouveau fetcher reste assez accessible. Pour télécharger les images Docker, le layer meta-dockin ajoute un fetcher Docker implémenté dans le fichier lib/dockin/dockerfetcher.py. Ce fetcher supporte les URLs de type docker (i.e. commençant par docker://) :

class Docker(FetchMethod):

    def supports(self, ud, d):
        return ud.type in ['docker']

En raison des problèmes évoqués précédemment sur l'usage du daemon docker, il n'est pas possible d'utiliser le CLI docker pour télécharger les images. Par conséquent, l'outil skopeo est utilisé et représente une bonne alternative rootless & serviceless.

Afin de préparer le téléchargement de l'image Docker via skopeo, il est nécessaire de manipuler les données de l'URL en amont :

def urldata_init(self, ud, d):
    if ud.path == '/':
        ud.path = ud.host
        ud.host = ""

    ud.basename = os.path.basename(ud.path)

    if 'tag' in ud.parm:
        ud.tag = ud.parm['tag']
    else:
        ud.tag = 'latest'

    ud.localfile = d.expand(urllib.parse.unquote(ud.basename)) + '_uncompressed_' + ud.tag

    ud.basecmd = d.getVar("FETCHCMD_docker") or "/usr/bin/env skopeo"

    # Replace aarch64 to arm64 for skopeo
    ud.target_arch = d.getVar("TARGET_ARCH")
    if ud.target_arch == 'aarch64':
        ud.target_arch = 'arm64'

La fonction urldata_init réalise ici plusieurs choses :

  1. Accepte les URLs docker://<image> faisant référence aux images officiels de docker.io en considérant <image> comme le chemin et nom l'hôte de l'URL.
  2. Utilise latest comme tag de l'image par défaut.
  3. Définit la commande d'exécution skopeo.
  4. Définit l'architecture cible pour le téléchargement de l'image.

Le téléchargement de l'image Docker se limite donc à l'appel de la commande Skopeo :

def download(self, ud, d):
    cmd = '%s copy --override-arch %s docker://%s%s:%s docker-archive:%s:%s:%s' % 
        (ud.basecmd, ud.target_arch, ud.host, ud.path, ud.tag, ud.localpath, ud.localfile, ud.tag)
    bb.fetch2.check_network_access(d, cmd, ud.url)

    runfetchcmd(cmd, d, False)

    return True

en savoir plus
La page de documentation de la commande skopeo copy présente les différentes options disponibles.

Il suffit ensuite d'ajouter notre fetcher au module :

bb.fetch2.methods.append(Docker())
bb.debug(1, "dockerfetcher: installed")

note
Si votre registry est privée il est nécessaire de faire un skopeo login avant de lancer bitbake.

Classe dockin

La classe bitbake dockin facilite l'intégration des images Docker dans le rootfs et prépare leur installation par le service de preload.

PV 'latest'

Dans l'idée qu'une recette équivaut à l'intégration d'une image Docker, la valeur par défaut de PV est remplacée par latest afin de l'utiliser comme le tag de l'image Docker. Le tag peut être aussi spécifié dans l'URL de SRC_URI, tel que :

SRC_URI += "docker://hello-world;tag=linux"

Compression des archives

Les images Docker téléchargées par Skopeo sont des archives non compréssées. Pour réduire leur taille sur le rootfs, la classe dockin compresse les archives en ajoutant une postfunc après la tâche do_unpack. La méthode de compression peut être modifiée en modifiant ces trois variables :

COMPRESSION_TYPE ?= "xz"
COMPRESSION_CMD:xz ?= "xz -f -k -9 -T0 -c"
COMPRESSION_RDEPENDS:xz ?= "xz"

Le nom du fichier de l'archive est renommé en <image_name>:<image_tag> afin de simplifier son chargement par le script de preload.

Installations des archives

Les archives compréssées des images Docker sont installées dans le dossier /usr/share/dockin/images.

Service preload

La recette dockin.bb de recipes-support/dockin installe le script de preload et son service systemd. Le script s'exécute à chaque démarrage, après Docker, les archives Docker présentes dans /usr/share/dockin/images sont chargées si nécessaire.

Pour gagner en temps de boot, il est donc important de placer le data-root de Docker dans un stockage persistant. De ce fait, les images Docker sont en réalité chargées qu'au first boot et les archives peuvent même être supprimées ― et le service systemd désactivé ― pour gagner de l'espace.

Exemples

Des recettes exemples sont disponibles dans le dossier recipes-demo/dockin.

Bibliographie

Meta-virtualization, https://github.com/lgirdk/meta-virtualization/
Docker-moby_git.bb, https://github.com/lgirdk/meta-virtualization/blob/master/recipes-containers/docker/docker-moby_git.bb
OCI, https://www.docker.com/blog/demystifying-open-container-initiative-oci-specifications/
Fstype, https://docs.yoctoproject.org/ref-manual/variables.html#term-IMAGE_FSTYPES
Bruce Ashfield, https://github.com/zeddii
Importing Docker Images*, https://lists.yoctoproject.org/g/meta-virtualization/message/6236
Building and deploying containers with meta-virtualization: now & in the future, https://youtu.be/HDSyILDGwfE?si=OX3n5XdjCyuByrII&t=1060
Docker rootless, https://docs.docker.com/engine/security/rootless/
Présentera des erreurs de droits, https://lists.yoctoproject.org/g/yocto/topic/error_when_try_to_use_sudo/96733939
Meta-embedded-containers, https://github.com/savoirfairelinux/meta-embedded-containers/tree/main
OCI/Docker containers with meta-virtualization and OE/the Yocto Project, https://youtu.be/n9NFRWzqdOA?si=BuScnAPi7p0XpHAD&t=1551
Meta-dockin, https://gitlab.com/PierreLoupG/meta-dockin
Docker compose, https://docs.docker.com/compose/compose-application-model/
Hello-world, https://hub.docker.com/_/hello-world
Fetch, https://docs.yoctoproject.org/bitbake/2.6/bitbake-user-manual/bitbake-user-manual-fetching.html
Skopeo, https://github.com/containers/skopeo
Documentation de la commande skopeo copy, https://github.com/containers/skopeo/blob/main/docs/skopeo-copy.1.md