Accélérer Le Démarrage De Vos Services Sur AWS Fargate avec SOCI

Accélérer le démarrage de vos services sur AWS Fargate avec des indexes SOCI

Un des problèmes avec Fargate est le temps démarrage d’une nouvelle tâche qui comprend les étapes suivantes1:

  1. La création et la mise en place de l’interface réseau. Chaque tâche se voit assignée une nouvelle adresse IP dans un sous réseau du VPC.
  2. La récupération des secrêts attachés à la task definition depuis Secret Manager.
  3. Le téléchargement des images Docker de la tâche depuis un registry (DockerHub, ECR, etc.).
  4. Et enfin le lancement des conteneurs spécifiés dans la task definition.

L’étape 3 est sans aucun doute celle qui pénalise le plus lorsqu’on utilise Fargate, en témoigne cette issue sur GitHub datant de 2020 et comptabilisant à date près de 600 👍. Chaque tâche démarrant une nouvelle micro VM Firecracker, nous n’avons pas de cache comme nous pourrions l’avoir avec un cluster ECS sur EC2.

Une étude a montré que 76% du temps de démarrage d’un conteneur sert à télécharger l’image depuis un registry alors que seulement 6.7% de ces données sont nécessaires au démarrage du conteneur.

Il y a bien évidemment plusieurs techniques pour optimiser le temps de démarrage. La plus évidente (à part utiliser un cluster EC2) est de réduire la taille des images Docker en utilisant des images alpine ou distroless, en faisant des builds multi-stage pour ne garder que le strict nécessaire dans l’image finale ou en changeant le mode de compression des images (zstd, par exemple).

Une autre solution est d’augmenter le CPU de votre service (attention au porte-monnaie), car cela a un impact sur la décompression complète de l’image qui se fait avec les capacités allouées au service (voir ici et ici).

Si vous n’êtes pas en mesure de réduire considérablement la taille de vos images (car vous faites du machine learning ou de l’IA, par exemple) et que vous souhaitez toujours utiliser Fargate, sachez que depuis juillet 2023, vous pouvez désormais utiliser des indexes SOCI avec Fargate (prononcez so-CHEE) pour Seekable OCI (Open Container Initiative).

Spoiler : vous pouvez augmenter le temps de démarrage jusqu’à 35% avec SOCI.

De quoi parle-t-on concrètement ? Découvrons cela ensemble avec une mise en pratique sur un de mes projets à titre d’exemple : lancement d’agent Jenkins dans une chaine CI/CD. Vous verrez également comment mettre en place des indexes à l’échelle au sein de votre entreprise sans modifier une seule ligne de code dans vos chaines d’intégration continue.

Table des matières

Du lazy loading à la place du cache

Avant de parler de SOCI, nous devons tout d’abord parler de snapshotter containerd. Le snapshotter est le composant responsable de la gestion des … snapshots des conteneurs, un snapshot étant est une copie immuable de l’état d’un système de fichiers à un moment donné. Lors du démarrage d’un conteneur, le snapshotter est utilisé pour créer le système de fichiers initial du conteneur en lecture seule.

Architecture de containerd avec une mise en avant des snapshotters. Source: https://containerd.io/
Architecture de containerd avec une mise en avant des snapshotters. Source: https://containerd.io/

SOCI est un snapshotter containerd (plus précisément un plugin). Il fonctionne sur le principe du lazy loading (ou chargement différé). Au lieu d’attendre la fin du téléchargement et la décompression complète de l’image avant de lancer le conteneur (comme le fait le snapshotter par défaut overlayfs), seuls les fichiers nécessaires ou utilisés au démarrage sont téléchargés en utilisant un index. Cet index fait le lien entre les layers de l’image, les fichiers qui se trouvent dans ce layer et la position des fichiers dans l’archive (offset et span). Cet index est stocké avec l’image dans le registre (seul l’ECR privé est supporté pour le moment) et peut être construit en déhors du build de l’image comme nous le verrons plus bas.

L’image suivante décrit le lien entre les différents composants :

Vu conceptuel de l'organisation de l'index SOCI. Source: [blog AWS](https://aws.amazon.com/fr/blogs/containers/under-the-hood-lazy-loading-container-images-with-seekable-oci-and-aws-fargate/)
Vu conceptuel de l’organisation de l’index SOCI. Source: blog AWS

Sur le schéma précédent, nous avons : en haut à gauche, le manifest par défaut qu’on retrouve dans les images. Il contient des méta données sur l’image, à savoir la liste des layers, le digest (identifiant unique de l’image), la taille, etc. En haut à droite, nous avons le manifest SOCI qui a un zTOC ( d’autres méta données) pour chaque layer de l’image initiale.

En bas à droite, le zTOC associé au layer avec la position de chaque fichier pour pouvoir les télécharger individuellement. Et en bas à gauche, la réprésentation d’un layer de l’image avec la liste des fichiers qui ont été modifiés.

Que se passe-t-il donc lors du lancement du conteneur ?

  1. La première fois où l’on va essayer d’accéder à un fichier, le snapshotter va d’abord vérifier si le fichier existe dans son cache. Si c’est le cas, le fichier est directement récupéré.
  2. Si le fichier n’existe pas en local, l’index (téléchargé en amont) est utilisé pour identifier le layer dans lequel se trouve le fichier ainsi que la position du fichier dans le layer. Ce fichier est alors téléchargé et sauvegardé en local. Cette opération est répétée pour chaque fichier accédé lors du démarrage du conteneur. En parallèle, un processus en tâche de fond télécharge également toute l’image.

Le principe de SOCI reste donc assez simple : télécharger uniquement ce dont nous avons besoin pour démarrer un conteneur et non toute l’image. Il marche très bien pour des services qui ne lisent qu’un “petit nombre” de fichiers au démarrage.

A contrario, pour un service qui lit un “grand nombre” de fichiers au démarrage, les gains ne sont pas garantis. AWS recommande de l’utiliser sur des images qui font au moins 250 MB.

Lors de mes premiers tests en Septembre 2023, je suis tombé sur un bug qui faisait qu’un conteneur ne démarrait plus après avoir rajouté l’index SOCI. Ce bug a été corrigé depuis par AWS.

Mise en pratique

Une des objectifs d’AWS est de ne pas modifier l’image dans le cadre de la mise en place de SOCI : la création de l’index se fait après et non durant le build de l’image.

Ainsi, nous avons deux manières de construire l’index SOCI :

Création manuelle avec la ligne de commande SOCI

Il s’agit de la méthode où nous construisons l’index en utilisant directement le binaire SOCI disponible dans le dépôt Git. Cette méthode nécessite néanmoins d’avoir en local les images dans le store containerd pour que soci puisse y accéder. Docker Engine dispose du support de containerd pour le stockage des images, mais cela est encore en phase expérimentale et ne sera pas abordé ici. Voir cette issue pour plus de détails.

Pour contourner cette limitation, AWS propose le Containerized SOCI Index Builder. C’est cette méthode que j’ai utilisée pour mes tests par souci de “simplicité”. La construction de l’index se fait dans un conteneur Docker avec toutes les dépendances préinstallées. Le projet Terraform Jenkins sur AWS dispose d’une nouvelle variable à passer à true pour automatiquement construire l’index SOCI à partir de mes images sur DockerHub.

Exemple d’utilisation : Ici, nous construisons et poussons l’index SOCI pour l’image ECR xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/jenkins-alpine-agent-aws:latest-alpine :

docker pull elmhaidara/soci-index-builder:21ec5445ea5e0908861e60e92cbdcd70d3251c93
docker run \
	--rm \
	--privileged \
	--env AWS_REGION=us-east-1 \
	--mount type=tmpfs,destination=/var/lib/containerd \
	--mount type=tmpfs,destination=/var/lib/soci-snapshotter-grpc \
	--platform linux/amd64 \
	--volume ${HOME}/.aws:/root/.aws \
	elmhaidara/soci-index-builder:21ec5445ea5e0908861e60e92cbdcd70d3251c93 \
	xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/jenkins-alpine-agent-aws:latest-alpine

Une fois la commande terminée, l’index sera disponible dans ECR et il suffit de re-démarrer le service ou lancer une tâche pour commencer à utiliser SOCI. Cette méthode est pratique pour faire des tests rapides sans déployer d’infrastructure supplémentaire comme dans la seconde solution.

J’ai mis à disposition une image Docker avec pour le containerized index builder sur DockerHub.

Utilisation de la lambda Index Builder (recommandée)

Pour une solution à long terme à déployer au sein d’une entreprise, AWS propose un ensemble de functions lambda déclenché via EventBridge de manière asynchrone pour chaque image poussée dans ECR : AWS SOCI Index Builder. Un template Cloudformation est également disponible.

La principale lambda écrite en Go va récupérer l’image, construire l’index et le pousser sur ECR. Une autre lambda se charge de filter les images pour lesquelles nous voulons construire un index.

Cette solution présente l’avantage de ne requérir aucune modification dans les processus de build existant : elle est totalement transparente du point de vue des développeurs qui n’ont pas à se soucier des indexes SOCI. Nous pouvons imaginer le scénario où cette infra sera déployée et maintenue par l’équipe platform.

Néanmoins, de par sa nature asynchrone, notez que l’index ne sera pas immédiatement disponible après le push de l’image dans ECR. Prévoyez un délai de 1 à 3 minutes en fonction de la taille de l’image avant d’utiliser SOCI. Ce facteur est important lors du déploiement immédiat d’une nouvelle version d’un service directement après la construction de l’image.

Comment vérifier que SOCI est bien utilisé ?

Pour vérifier que SOCI est bien utilisé par une tâche, il suffit de lire les méta données de la tâche ECS via l’endpoint $ECS_CONTAINER_METADATA_URI_V4/task depuis le conteneur et de vérifier la valeur de la clé Snapshotter qui doit être égale à soci. Autrement, vous verrez overlayfs comme valeur.

Le job par défaut créé par le projet Terraform affiche les méta données de la tâche dans la console Jenkins.

AWS propose également un outil à déployer avec votre service pour voir si SOCI est bien utilisé.

Et les chiffres?

Sur le papier, SOCI semble prometteur, mais qu’en est-il réellement ? Sans plus attendre, voici quelques chiffres tirés de mes tests sur mon projet Jenkins sur Fargate:

  • Image du controller version 2.433: 1.12 GB.
  • Image de l’agent version 3192.v713e3b_039fb_e-4-alpine-jdk17: 0.315 GB.
  • Chiffres sur 16 lancements.
  • Le temps est exprimé en secondes.
Chiffres avec et sans SOCI pour le **contrôleur Jenkins**
Chiffres avec et sans SOCI pour le contrôleur Jenkins
Chiffres avec et sans SOCI pour les **agents Jenkins**
Chiffres avec et sans SOCI pour les agents Jenkins

En résumé, en moyenne, les temps de démarrage du contrôleur et de l’agent sont réduits respectivement de 36 % et 19 %. On aurait aimé que l’agent démarre encore plus rapidement, mais cela peut s’expliquer par sa faible taille. Si vous avez de plus grosses images avec beaucoup de dépendances, les gains devraient être plus significatifs.

Ces chiffres ont été obtenus en capturant et analysant les évènements ECS de changement d’état des tâches. Ces évènements contiennent entre autres les dates de création et de lancement de la tâche ainsi que les dates début et fin du téléchargement de l’image. Voir ce module pour les scripts qui ont été utilisés pour faire ces tests.

N’hésitez pas à faire vos tests et à partager vos résultats dans les commentaires.

Pour terminer

En conclusion, l’intégration de SOCI avec Fargate offre une approche intéressante pour reduire le temps de démarrage des conteneurs. Elle ne pourra néanmoins pas remplacer un vrai cache avec une image déjà stockée en local comme sur EC2, mais sa relative facilité de mise en place en fait un bon candidat à tester au sein de votre infrastructure en fonction de vos cas d’usage.


  1. Plus d’information dans le livre blanc sur la sécurité dans Fargate ( page 13). ↩︎

Mohamed El Mouctar HAIDARA
Mohamed El Mouctar HAIDARA
Senior Platform Engineer
comments powered by Disqus