Utiliser S3 comme registre de conteneurs
(ochagavia.nl)- Les images de conteneurs sont généralement publiées dans un registre dédié, mais si l’on expose un bucket S3 en HTTP et que l’on place les fichiers dans les chemins attendus, il peut être utilisé comme cible pour
docker pull - Dans un builder d’images personnalisé, le processus permettant de produire en quelques secondes une image pouvant être pullée était devenu un goulot d’étranglement, le temps de push des couches représentant une part importante
- Dans un benchmark d’upload d’une couche de 198 Mio, ECR a été mesuré à 24–28 Mio/s, contre 115–190 Mio/s pour S3, soit jusqu’à 8 fois plus rapide pour S3
- L’écart vient du fait que S3 peut uploader en parallèle les chunks d’une même couche, tandis qu’ECR, fondé sur l’OCI Distribution Spec, doit traiter les chunks séquentiellement
- Cette approche ne remplace pas les fonctionnalités de registre comme
docker push, la validation des images, l’analyse de sécurité ou le contrôle d’accès aux dépôts privés ; elle doit donc être vue comme une optimisation expérimentale
Conditions pour que docker pull fonctionne depuis un bucket S3
- Pour utiliser S3 comme un registre de conteneurs, il faut exposer le bucket en HTTP et uploader les fichiers de l’image aux chemins attendus par Docker
- Si les conditions sont réunies, l’image peut être récupérée avec
docker pullcomme depuis un registre classique - L’image de démonstration exécute cowsay, et fonctionne avec
docker run --rm .../cowsayen utilisant l’URL du bucket comme nom d’image - La démonstration utilise Cloudflare R2
- L’egress gratuit est la raison de ce choix
- R2 et S3 étant compatibles au niveau API, l’image est uploadée vers R2 avec l’AWS SDK
Pourquoi envisager S3 au lieu d’un registre dédié
- Les images de conteneurs sont généralement hébergées dans des registres dédiés comme DockerHub, GitHub Container Registry ou ECR
- L’objectif du builder d’images personnalisé est de passer des exigences à une image pouvant être pullée en quelques secondes
- Dans un environnement AWS, ECR est le choix le plus simple, mais la différence entre S3 et ECR est apparue nettement dans les vitesses d’upload réelles
- En ajoutant du traçage d’exécution au code pour optimiser les performances, le temps passé à pousser les couches vers le registre de conteneurs s’est révélé être un goulot d’étranglement majeur
Résultats d’upload d’une couche de 198 Mio
- Un petit benchmark a comparé le temps nécessaire et le débit pour uploader une couche de 198 Mio vers ECR et vers S3
- Les vitesses observées sont les suivantes
- ECR : minimum 24 Mio/s, 8,2 s
- ECR : maximum 28 Mio/s, 7,0 s
- S3 : minimum 115 Mio/s, 1,7 s
- S3 : maximum 190 Mio/s, 1,0 s
- D’après ces résultats, S3 atteint un niveau jusqu’à 8 fois plus rapide qu’ECR
- Le code expérimental a été exécuté sur AWS, et S3 comme ECR étaient connectés en interne via un VPC, sans passer par l’Internet public
- Cette configuration offre une latence et une bande passante aussi favorables que possible
L’écart de vitesse créé par l’upload parallèle de chunks
- Avec S3, les chunks d’une même couche peuvent être uploadés en parallèle
- Si la bande passante est suffisante, l’upload parallèle de chunks augmente fortement le débit
- La documentation AWS recommande également l’upload parallèle de chunks afin de maximiser l’utilisation de la bande passante
- ECR implémente l’OCI Distribution Spec
- Cette spécification est le standard qui permet à
docker pulletdocker pushde fonctionner avec plusieurs registres - Le push des couches doit se dérouler séquentiellement : même avec un upload par chunks, il faut attendre la fin du chunk précédent avant de passer au suivant
- Cette spécification est le standard qui permet à
- Lorsqu’on teste aussi un upload séquentiel sur S3, le débit retombe à un niveau similaire à celui d’ECR
Structure réelle des requêtes de docker pull
- Les requêtes internes de
docker pullse composent de plusieurs requêtes HEAD et GET - Un flux typique ressemble à ceci
- Vérifier l’existence du manifeste d’image :
HEAD /v2/my-image/manifests/latest - Télécharger le manifeste d’image :
GET /v2/my-image/manifests/latest - Télécharger à nouveau via le hash du manifeste :
GET /v2/my-image/manifests/sha256:... - Télécharger le blob de métadonnées de l’image :
GET /v2/my-image/blobs/sha256:... - Télécharger le blob de couche de l’image :
GET /v2/my-image/blobs/sha256:...
- Vérifier l’existence du manifeste d’image :
- Au final,
docker pullressemble surtout à un processus de téléchargement HTTP des fichiers nécessaires - Si un serveur de fichiers statiques place les fichiers nécessaires aux chemins attendus et définit l’en-tête Content-Type approprié pour chaque requête, il est possible de puller une image de conteneur
- Un bucket S3 peut remplir ces deux conditions ; configuré avec prudence, il peut donc se comporter comme un registre de conteneurs
Limites de cette approche expérimentale
- Cette approche reste expérimentale, et il est difficile de tirer des conclusions fortes sans enquête plus approfondie
- S3 n’est pas, au sens strict, un registre de conteneurs
- Il n’est pas possible de faire un
docker push - Le fait que
docker pullfonctionne est le résultat de l’adéquation entre la structure des requêtes HTTP et la fourniture de fichiers statiques
- Il n’est pas possible de faire un
- Les registres de conteneurs existants offrent davantage de fonctionnalités qu’un simple dépôt de fichiers dans un bucket
- On peut avoir confiance dans le fait qu’une image uploadée via la méthode standard de push est réellement valide
- Ils peuvent fournir des analyses de sécurité automatiques des couches et des alertes
- Ils permettent de définir nativement les droits d’accès aux dépôts privés
- Si cela fonctionne comme prévu, il pourrait aussi devenir possible d’héberger des images de conteneurs publiques sur Cloudflare R2
1 commentaires
Avis sur Hacker News
Il est dommage que l’OCI Distribution Spec ne se lise pas comme une spécification bien conçue
D’après la spécification, le push des couches doit se faire séquentiellement : même en uploadant par morceaux, il faut attendre la fin de chaque morceau avant de passer au suivant. D’après des tests sur DockerHub et GHCR, l’upload par morceaux lui-même est de toute façon cassé, et les clients ont plutôt tendance à envoyer chaque blob/couche en entier. La spécification recommande aussi un format de valeur
Content-Rangequi ne correspond pas au format RFC7233Bien sûr, il y a du parallélisme au niveau des blobs, mais pas à l’intérieur d’un blob. Je regrette aussi l’occasion manquée de standardiser la pagination de la liste des tags. Une formulation à ce sujet a été supprimée par erreur de la norme [1], ce qui a conduit chaque registre à l’implémenter à sa manière
[1] https://github.com/opencontainers/distribution-spec/issues/4...
Je ne veux pas pour autant le dénigrer complètement. Cela a réellement été révolutionnaire, a rendu l’usage des namespaces Linux bien plus simple qu’avant, et a changé le monde dans le bon sens. Mais Docker a toujours privilégié l’expérience utilisateur à la perfection technique, et ce n’est pas forcément mauvais en soi. Comme il existe beaucoup d’entreprises ennuyeuses qui résolvent des problèmes coûteux avec Perl ou des CSV échangés par FTP, une technologie ennuyeuse, voire mauvaise, peut avoir beaucoup de valeur si elle est livrée dans un bon package
Malgré tout, c’est parfois un peu amer de se dire que cela aurait pu être bien meilleur qu’aujourd’hui
Par exemple, une forme comme
.dkr.ecr..amazonaws.com/foo/bar/baz:tagne fonctionne pas ; seul un stockage à plat est possible, ce qui rend les noms d’images ou les tags excessivement longs. En théorie, on peut créer des objets de dépôt ECR dans Terraform pour imiter quelque chose de similaire, mais ce n’est pas idéal dans des pipelines où le chemin final de l’image est dynamique. Il faut accorder au rôle IAM du pipeline CI un nombre de permissions inconfortablement élevé, et je n’aime pas non plus que des ressources AWS soient gérées en dehors du dépôt Terraform central[1] https://stackoverflow.com/questions/64232268/storing-images-...
Cloudflare a déjà publié en open source un serveur de registre de conteneurs utilisant R2
Je me demande si quelqu’un l’a essayé
[1]: https://github.com/cloudflare/serverless-registry
Dans certains cas d’usage, ce n’est peut-être pas un gros problème, mais dans d’autres, cela peut être un critère d’exclusion immédiat
Je suis l’auteur de l’article. Si quelqu’un sait pourquoi la spécification OCI impose que le push des couches soit séquentiel, j’aimerais bien le savoir
Je me demande si c’est simplement un accident historique ou s’il y a une raison cachée. Pour être clair, plusieurs couches peuvent évidemment être poussées en parallèle ; ici, je parle de la partie qui impose de pousser le contenu d’une seule couche de manière séquentielle
N+Timeout, il s’agit d’un upload non terminé, et qu’on peut donc le supprimerCela simplifie le détail d’implémentation consistant à décider quoi faire des uploads partiels. Sinon, à la fin de chaque chunk, il faudrait vérifier que tous les autres chunks sont présents et marquer l’upload comme terminé. Cela dit, c’est un détail d’implémentation, et je doute que ce soit une conception significative ou intentionnelle. L’approche S3 devrait bien fonctionner, et j’ai déjà fait quelque chose de similaire dans une entreprise qui déployait de grosses images. Les 0,10 dollar par Go et par mois finissaient par représenter une somme importante
On perd les fonctionnalités supplémentaires d’ECR, mais personnellement je les trouve assez limitées
Lorsque le client termine l’upload d’un blob, il doit fournir le digest du blob complet. Cette exigence semble destinée à permettre au serveur de vérifier l’intégrité des octets reçus. Si le serveur ne commence à vérifier le digest qu’au moment de la dernière requête HTTP, il doit relire tout le contenu du blob déjà écrit dans le stockage lors des requêtes HTTP précédentes. Pour de grosses couches, cette latence peut devenir difficilement acceptable. Pour répondre aux besoins d’un client particulier, nous avons vérifié que cela fonctionnait jusqu’à des blobs de 150 Gio
Dans notre implémentation, en revanche, nous poursuivons le calcul du digest tout au long de la séquence de requêtes. À mesure que nous recevons les données du blob par chunks, nous calculons simultanément le digest et les envoyons en streaming vers le stockage de blobs. Entre chaque requête, nous sérialisons l’état du calcul du digest dans l’URL d’upload renvoyée au client via l’en-tête
Location. C’est à peu près géré dans ce code : https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...À ma connaissance, l’implémentation de référence utilise la même approche. Comme le calcul du digest ne peut se faire que séquentiellement, l’upload doit lui aussi se dérouler de manière séquentielle
[1] https://github.com/sapcc/keppel
[2] https://github.com/distribution/distribution
Même un lien vers le dépôt GitHub serait appréciable. Le contexte, c’est que je cherche un moyen de créer des images OCI de manière programmatique depuis
$PROGRAMMING_LANGUAGE, ou que j’envisage de l’implémenter moi-même. Quelque chose comme Buildah, mais sous forme d’API pour un vrai langage de programmation, pas d’interface en ligne de commande. Bien sûr, on peut appeler Buildah comme sous-processus, mais c’est assez pénible, il faut aussi gérer l’interaction avec l’état interne de Buildah ou son nettoyage, et Buildah ne prend actuellement pas en charge le MacIl me semble avoir ajouté autrefois le push parallèle à Docker, mais je confonds peut-être avec le pull et le push. En y repensant, mon travail portait sur la parallélisation des vérifications, pas sur le push final. Si l’on précise sur quelle couche une couche vient se placer, il est possible que l’ID référencé doive déjà exister
C’est un cas d’usage plutôt chouette
Personnellement, j’utilise simplement Nexus. Ça marche suffisamment bien et ça prend en charge aussi bien les images OCI que les paquets apt, les dépôts Maven personnalisés, NuGet, npm, etc. En revanche, la configuration et l’utilisation des ressources sont un peu pénibles, surtout du côté des politiques de nettoyage : https://www.sonatype.com/products/sonatype-nexus-repository
Cela dit, j’aime vraiment le fait que
docker pullne soit “qu’un” ensemble de requêtesHEADetGET. J’aimerais voir plus de technologies prendre ce genre de décision de bon sens : réutiliser ce qui fonctionne bien depuis longtemps, sans compliquer inutilement les choses. Je suis surpris qu’il n’existe pas davantage de stockages de conteneurs simples avec authentification et nettoyage. Nexus et Harbor sont tous deux assez complexes à utiliser en pratiqueJe suis surpris que personne ne l’ait mentionné dans ce fil
Distribution de la CNCF, l’ancien Docker Registry, inclut une fonctionnalité permettant d’adosser le registre à des URL signées CloudFront qui récupèrent les données depuis S3 [1]
https://distribution.github.io/distribution/storage-drivers/...
Je me demande quel est le problème avec https://github.com/distribution/distribution
Cette approche semble très coûteuse, et j’aurais aimé que l’article aborde aussi les coûts. Je suis curieux pour S3 comme pour R2.
Le coût du trafic sortant vers Internet gratuit est le même, mais les dépôts ECR publics ont une exception : le trafic sortant pour une utilisation interne à AWS est gratuit.
GET/PUTet les coûts de bande passante sont consultables sur le site d’AWS : https://aws.amazon.com/s3/pricing/En dehors des outils de développement, je n’utilise pas beaucoup Docker, mais je n’ai jamais compris pourquoi des registres de conteneurs privés devaient exister.
Ça ressemble juste à de la rente. Je me demande quel avantage concret cela apporte par rapport à créer soi-même une sorte de fichier image géré directement et à l’utiliser comme on veut.
docker saveetdocker import.docker save alpine:3.19 > alpine.tardocker load < alpine.tarMais il faut alors gérer ce fichier tar, et tous les systèmes doivent savoir où il se trouve et comment y accéder. Sinon, on peut éviter de réinventer la roue et utiliser le mécanisme que Docker fournit déjà.
Il faut donc mettre en place son propre registre, ou payer quelqu’un pour le faire. Bien sûr, si vous n’utilisez des images que pour le développement, tout cela n’a pas d’importance : il suffit de les stocker sur la machine de développement.
On a besoin d’un dépôt central qui conserve toutes les versions précédentes et auquel plusieurs consommateurs peuvent accéder facilement. On n’a pas envie, après avoir buildé une app, de la pousser manuellement partout où elle pourrait s’exécuter. Il suffit de builder une fois, de pousser vers un dépôt central, puis de faire en sorte que tous les environnements y fassent référence.
Il n’est pas non plus nécessaire de payer pour l’hébergement d’un dépôt privé. Il existe beaucoup d’outils qu’on peut héberger soi-même.
On peut tout configurer par environnement avec Terraform, Bicep ou Pulumi.
ECR semble en réalité conçu pour permettre de téléverser les couches d’image en plusieurs parties.
Parmi les API ECR concernées, il y a
InitiateLayerUpload API, appelée au début du téléversement de chaque couche d’image,UploadLayerPart API, appelée pour chaque fragment de couche (jusqu’à 20 Mo), etPutImage API, qui pousse le manifeste d’image contenant les références aux couches après le téléversement. Ce qui est étrange, c’est que les fragments de couche doivent être téléversés en encodage base64, ce qui augmente les données d’environ 33 %.L’idée d’utiliser l’agencement des chemins de fichiers comme moyen de contrôle des endpoints est intéressante.
Je me demande toutefois comment gérer l’en-tête
Docker-Content-Digest. Il n’est pas obligatoire, mais il est recommandé de l’inclure dans la réponse, et de nombreux clients s’y attendent et peuvent refuser une couche sans cet en-tête. On risque aussi de passer à côté de fonctionnalités comme la referrers API de la spécification OCI 1.1. L’implémentation semble assez délicate.