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
etunshare
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 variableSRC_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 :
- Accepte les URLs
docker://<image>
faisant référence aux images officiels dedocker.io
en considérant<image>
comme le chemin et nom l'hôte de l'URL. - Utilise
latest
comme tag de l'image par défaut. - Définit la commande d'exécution
skopeo
. - 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
Il suffit ensuite d'ajouter notre fetcher au module :
bb.fetch2.methods.append(Docker())
bb.debug(1, "dockerfetcher: installed")
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.