S3 als Container-Registry verwenden
(ochagavia.nl)- Container-Images werden normalerweise in einer dedizierten Registry abgelegt, aber wenn ein S3-Bucket per HTTP verfügbar gemacht und Dateien an den vorgegebenen Pfaden abgelegt werden, kann er wie ein Ziel für
docker pullverwendet werden - Bei einem benutzerdefinierten Image-Builder wurde der Prozess, ein pullbares Image innerhalb weniger Sekunden zu erzeugen, zum Flaschenhals, wobei das Pushen der Layer einen großen Anteil der Zeit ausmachte
- Beim Upload-Benchmark für einen 198-MiB-Layer wurde ECR mit 24–28 MiB/s und S3 mit 115–190 MiB/s gemessen, womit S3 bis zu 8-mal schneller war
- Der Unterschied entsteht dadurch, dass S3 Chunks eines einzelnen Layers parallel hochladen kann, während ECR auf Basis der OCI Distribution Spec Chunks sequenziell verarbeiten muss
- Dieser Ansatz kann Registry-Funktionen wie
docker push, Validierung von Images, Security-Scans oder Zugriffskontrolle für private Repositories nicht ersetzen und sollte daher als experimentelle Optimierung betrachtet werden
Bedingungen, unter denen docker pull aus einem S3-Bucket funktioniert
- Um S3 wie eine Container-Registry zu verwenden, muss der Bucket per HTTP verfügbar gemacht werden, und die Image-Dateien müssen an den von Docker erwarteten Pfaden hochgeladen werden
- Wenn diese Bedingungen erfüllt sind, lässt sich das Image wie aus einer normalen Registry per
docker pullabrufen - Das Demo-Image führt cowsay aus und verwendet die Bucket-URL als Image-Namen, sodass es mit
docker run --rm .../cowsayfunktioniert - Im Demo wird Cloudflare R2 verwendet
- Ausschlaggebend war der kostenlose Egress
- R2 und S3 sind API-kompatibel, daher wird das Image mit dem AWS SDK zu R2 hochgeladen
Warum S3 statt einer dedizierten Registry betrachtet wurde
- Container-Images werden üblicherweise in dedizierten Registries wie DockerHub, GitHub Container Registry oder ECR gehostet
- Das Ziel des benutzerdefinierten Image-Builders ist es, innerhalb weniger Sekunden von einer Anforderung zu einem pullbaren Image zu kommen
- In einer AWS-Umgebung ist ECR die einfachste Wahl, aber bei der tatsächlichen Upload-Geschwindigkeit zeigte sich ein deutlicher Unterschied zwischen S3 und ECR
- Als zur Performance-Optimierung Traces in den Code eingebaut wurden, stellte sich heraus, dass das Pushen der Layer in die Container-Registry ein wesentlicher Flaschenhals war
Ergebnisse beim Upload eines 198-MiB-Layers
- Ein kleiner Benchmark vergleicht Upload-Zeit und Durchsatz, indem ein 198-MiB-Layer jeweils zu ECR und S3 hochgeladen wird
- Die beobachteten Geschwindigkeiten waren wie folgt
- ECR: mindestens 24 MiB/s, 8,2 Sekunden
- ECR: maximal 28 MiB/s, 7,0 Sekunden
- S3: mindestens 115 MiB/s, 1,7 Sekunden
- S3: maximal 190 MiB/s, 1,0 Sekunden
- Den Ergebnissen zufolge war S3 gegenüber ECR bis zu 8-mal schneller
- Der Versuchscode lief in AWS, und S3 sowie ECR waren intern über eine VPC verbunden, ohne das öffentliche Internet zu durchqueren
- Diese Bedingungen sorgen für möglichst gute Latenz und Bandbreite
Der Geschwindigkeitsunterschied durch parallelen Chunk-Upload
- Bei S3 können Chunks eines einzelnen Layers parallel hochgeladen werden
- Bei ausreichender Bandbreite steigert paralleler Chunk-Upload den Durchsatz erheblich
- Auch die AWS-Dokumentation empfiehlt parallelen Chunk-Upload, um die Bandbreitennutzung zu maximieren
- ECR implementiert die OCI Distribution Spec
- Diese Spezifikation ist der Standard, der
docker pullunddocker pushüber verschiedene Registries hinweg ermöglicht - Das Pushen von Layern muss sequenziell erfolgen; selbst bei Chunk-Uploads kann der nächste Chunk erst beginnen, wenn der vorherige abgeschlossen ist
- Diese Spezifikation ist der Standard, der
- Wenn in S3 ebenfalls ein sequenzieller Upload getestet wird, fällt der Durchsatz auf ein ähnliches Niveau wie bei ECR zurück
Die tatsächliche Anfragestruktur von docker pull
- Die internen Anfragen von
docker pullbestehen aus mehreren HEAD- und GET-Requests - Ein beispielhafter Ablauf sieht so aus
- Prüfen, ob ein Image-Manifest existiert:
HEAD /v2/my-image/manifests/latest - Image-Manifest herunterladen:
GET /v2/my-image/manifests/latest - Erneut per Manifest-Hash herunterladen:
GET /v2/my-image/manifests/sha256:... - Blob mit Image-Metadaten herunterladen:
GET /v2/my-image/blobs/sha256:... - Blob mit dem Image-Layer herunterladen:
GET /v2/my-image/blobs/sha256:...
- Prüfen, ob ein Image-Manifest existiert:
- Im Kern ist
docker pulldamit ein Vorgang, bei dem die benötigten Dateien per HTTP heruntergeladen werden - Wenn ein statischer Fileserver die benötigten Dateien an den erwarteten Pfaden bereitstellt und für jede Anfrage die passenden Content-Type-Header setzt, können Container-Images gepullt werden
- Ein S3-Bucket kann diese beiden Bedingungen erfüllen und mit vorsichtiger Konfiguration wie eine Container-Registry funktionieren
Grenzen dieses experimentellen Ansatzes
- Dieser Ansatz ist weiterhin experimentell, und vor weiteren Untersuchungen lassen sich nur schwer belastbare Schlussfolgerungen ziehen
- S3 ist im strengen Sinn keine Container-Registry
docker pushist nicht möglich- Dass
docker pullfunktioniert, ist das Ergebnis einer passenden HTTP-Anfragestruktur und statischen Dateiauslieferung
- Bestehende Container-Registries bieten deutlich mehr Funktionen als das einfache Ablegen von Dateien in einem Bucket
- Man kann darauf vertrauen, dass per Standard-Push hochgeladene Images tatsächlich gültig sind
- Sie können automatische Security-Scans und Warnungen für Layer bereitstellen
- Zugriffsrechte für private Repositories lassen sich nativ festlegen
- Wenn es wie erwartet funktioniert, könnte es auch möglich werden, öffentliche Container-Images in Cloudflare R2 zu hosten
1 Kommentare
Meinungen auf Hacker News
OCI Distribution Spec liest sich leider nicht wie eine gut entworfene Spezifikation.
Laut Spezifikation müssen Layer sequenziell gepusht werden, sodass man selbst beim Upload in Chunks erst zum nächsten weitergehen kann, wenn der jeweilige Chunk abgeschlossen ist. Nach meinen Tests mit DockerHub und GHCR ist der Chunk-Upload selbst ohnehin kaputt, und Clients laden eher jeden Blob/Layer am Stück hoch. Die Spezifikation empfiehlt außerdem ein Format für
Content-Range-Werte, das nicht zum RFC7233-Format passt.Zwar gibt es Parallelität auf Blob-Ebene, aber keine innerhalb eines Blobs. Außerdem ärgert mich, dass die Gelegenheit verpasst wurde, die Paginierung der Tag-Liste zu standardisieren. Durch das versehentliche Entfernen der entsprechenden Formulierung aus dem Standard [1] implementiert das nun jede Registry anders.
[1] https://github.com/opencontainers/distribution-spec/issues/4...
Damit will ich es nicht völlig verteufeln. Es war tatsächlich revolutionär, hat die Nutzung von Linux-Namespaces viel einfacher gemacht als zuvor und die Welt zum Besseren verändert. Es hat nur immer die User Experience über technische Vollständigkeit gestellt, und das ist an sich nicht unbedingt schlecht. So wie es viele langweilige Unternehmen gibt, die teure Probleme mit Perl oder per FTP ausgetauschten CSV-Dateien lösen, kann auch langweilige oder sogar schlechte Technik großen Wert haben, wenn sie gut verpackt ist.
Trotzdem ist es manchmal bitter, weil es so viel besser hätte sein können als heute.
Eine Form wie
.dkr.ecr..amazonaws.com/foo/bar/baz:tagfunktioniert zum Beispiel nicht; es ist nur flache Speicherung möglich, wodurch Image-Namen oder Tags übermäßig lang werden. Theoretisch könnte man in Terraform ECR-Repository-Objekte anlegen und etwas Ähnliches nachbilden, aber bei Pipelines mit dynamischen Zielpfaden für Images ist das nicht besonders gut. Man müsste der IAM-Rolle der CI-Pipeline unangenehm viele Berechtigungen geben, und mir gefällt auch nicht, dass AWS-Ressourcen außerhalb des zentralen Terraform-Repositorys verwaltet werden.[1] https://stackoverflow.com/questions/64232268/storing-images-...
Cloudflare hat einmal einen Open-Source-Container-Registry-Server veröffentlicht, der R2 verwendet.
Ich frage mich, ob ihn schon jemand ausprobiert hat.
[1]: https://github.com/cloudflare/serverless-registry
Für manche Einsatzzwecke ist das vielleicht kein großes Problem, für andere kann es sofort ein Ausschlusskriterium sein.
Ich bin der Autor. Falls jemand weiß, warum Layer-Pushes in der OCI-Spezifikation sequenziell sein müssen, würde ich mich über Hinweise freuen.
Ich frage mich, ob das einfach ein historischer Zufall ist oder ob es einen versteckten Grund gibt. Zur Klarstellung: Mehrere Layer kann man natürlich parallel pushen; hier geht es um den Teil, dass der Inhalt eines einzelnen Layers sequenziell gepusht werden muss.
N+Timeoutklar, dass es sich um einen nicht abgeschlossenen Upload handelt, den man löschen kann.Damit werden Implementierungsdetails dazu, wie partielle Uploads behandelt werden, vereinfacht. Andernfalls müsste man am Ende jedes Chunks prüfen, ob alle anderen Chunks vorhanden sind, und dann den Abschluss durchführen. Allerdings ist das ein Implementierungsdetail, und ich bezweifle, ob das eine sinnvolle oder beabsichtigte Designentscheidung war. Der S3-Ansatz dürfte gut funktionieren; bei einer Firma, die früher große Images ausgerollt hat, habe ich einmal etwas Ähnliches gemacht. 0,10 US-Dollar pro GB und Monat summierte sich ziemlich.
Die Zusatzfunktionen von ECR verliert man, aber persönlich halte ich diese Funktionen für ziemlich begrenzt.
Wenn ein Client einen Blob-Upload abschließt, muss er den Digest des gesamten Blobs angeben. Diese Anforderung scheint dazu zu dienen, dass der Server die Integrität der empfangenen Bytes prüfen kann. Wenn der Server erst beim letzten HTTP-Request mit der Digest-Prüfung beginnen würde, müsste er den gesamten Blob-Inhalt, den er bei früheren HTTP-Requests bereits in den Storage geschrieben hat, erneut lesen. Bei großen Layern kann diese Verzögerung untragbar sein. Wegen einer konkreten Client-Anforderung haben wir verifiziert, dass es bis zu 150-GiB-Blobs funktioniert.
Stattdessen führt unsere Implementierung die Digest-Berechnung über die gesamte Request-Sequenz hinweg fort. Während Blob-Daten chunkweise empfangen werden, werden gleichzeitig der Digest berechnet und die Daten in den Blob-Storage gestreamt. Zwischen den einzelnen Requests serialisieren wir den Zustand der Digest-Berechnung in der Upload-URL, die im
Location-Header an den Client zurückgegeben wird. Grob wird das in diesem Code behandelt: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...Soweit ich weiß, verwendet die Referenzimplementierung denselben Ansatz. Da die Digest-Berechnung nur sequenziell möglich ist, muss auch der Upload sequenziell ablaufen.
[1] https://github.com/sapcc/keppel
[2] https://github.com/distribution/distribution
Ein Link zum GitHub-Repository wäre auch schön. Hintergrund ist, dass ich nach einer Möglichkeit suche, OCI-Images programmatisch aus
$PROGRAMMING_LANGUAGEheraus zu erstellen, oder überlege, so etwas selbst zu implementieren. Also wie Buildah, aber nicht als Kommandozeileninterface, sondern als echte API für eine Programmiersprache. Natürlich könnte man Buildah als Subprozess aufrufen, aber das ist etwas umständlich; außerdem muss man sich um die Interaktion mit dem internen Zustand von Buildah und dessen Bereinigung kümmern, und derzeit unterstützt Buildah auch keinen Mac.Ich glaube, ich hatte früher einmal parallele Pushes zu Docker hinzugefügt, aber vielleicht verwechsle ich das mit Pull und Push. Später stellte sich heraus, dass meine Arbeit nicht der finale Push war, sondern eher die Parallelisierung von Prüfungen. Wenn man angibt, auf welchem Layer ein Layer „obenauf“ liegt, könnte es daran liegen, dass die referenzierte ID bereits existieren muss.
Ein ziemlich cooler Use Case.
Persönlich benutze ich einfach Nexus. Es funktioniert gut genug und unterstützt alles von OCI-Images über apt-Pakete bis zu eigenen Maven-, NuGet- und npm-Repositories. Allerdings sind Konfiguration und Ressourcenverbrauch etwas lästig, besonders bei den Bereinigungsrichtlinien: https://www.sonatype.com/products/sonatype-nexus-repository
Trotzdem ist es wirklich schön, dass
docker pull„einfach“ ein Bündel ausHEAD- undGET-Requests ist. Ich würde gerne bei mehr Technologien solche vernünftigen Entscheidungen sehen: Dinge weiterverwenden, die seit Langem gut funktionieren, und sie nicht unnötig verkomplizieren. Es überrascht mich, dass es nicht mehr einfache Container-Storages mit Authentifizierung und Bereinigung gibt. Nexus und Harbor sind beide ziemlich komplex für den praktischen Einsatz.Ich bin überrascht, dass es in diesem Thread niemand erwähnt hat.
CNCF Distribution, früher Docker Registry, enthält eine Funktion, um die Registry mit CloudFront Signed URLs zu hinterlegen, die von S3 abrufen [1].
https://distribution.github.io/distribution/storage-drivers/...
Ich frage mich, was an https://github.com/distribution/distribution das Problem ist.
Dieser Ansatz wirkt sehr teuer, und es wäre gut gewesen, wenn der Artikel auch die Kosten behandelt hätte. Mich würden sowohl S3 als auch R2 interessieren.
Die Kosten für ausgehenden Traffic ins freie Internet sind gleich, aber bei öffentlichen ECR-Repositories gibt es die Ausnahme, dass ausgehender Traffic für die Nutzung innerhalb von AWS kostenlos ist.
GET/PUTund für Bandbreite findet man auf der AWS-Website: https://aws.amazon.com/s3/pricing/.Außerhalb von Entwicklungstools nutze ich Docker nicht viel, aber ich habe nie verstanden, warum es private Container-Registries geben muss.
Für mich wirkt das einfach wie Rent-Seeking. Mich würde interessieren, welche konkreten Vorteile das gegenüber einer selbst verwalteten Image-Datei hat, die man nach Belieben verwendet.
docker saveunddocker importverwenden.docker save alpine:3.19 > alpine.tardocker load < alpine.tarAber dann muss man diese tar-Datei verwalten, und alle Systeme müssen wissen, wo sie liegt und wie sie darauf zugreifen. Oder man nutzt einfach den Weg, den Docker bereits bereitstellt, statt das Rad neu zu erfinden.
Deshalb muss man entweder selbst eine Registry betreiben oder jemanden dafür bezahlen. Wenn man Images natürlich nur für die Entwicklung nutzt, ist all das irrelevant; dann kann man sie einfach auf der Entwicklungsmaschine speichern.
Man braucht ein zentrales Repository, in dem alle früheren Versionen vorhanden sind und auf das mehrere Konsumenten einfach zugreifen können. Man möchte eine App nach dem Build nicht einzeln an jeden Ort schieben, an dem sie ausgeführt werden könnte. Stattdessen baut man einmal, pusht in ein zentrales Repository und lässt alle Orte auf dieses Repository verweisen.
Für das Hosting eines privaten Repositorys muss man auch nicht unbedingt bezahlen. Es gibt viele Tools, mit denen man es selbst hosten kann.
Man kann alles pro Umgebung mit Terraform, Bicep oder Pulumi konfigurieren.
ECR wirkt tatsächlich so, als sei es dafür ausgelegt, Image-Layer in mehreren Teilen hochladen zu können.
Zu den relevanten ECR-APIs gehören die
InitiateLayerUpload API, die beim Start des Uploads jedes Image-Layers aufgerufen wird, dieUploadLayerPart API, die für jeden Layer-Chunk aufgerufen wird (maximal 20 MB), sowie diePutImage API, mit der nach dem Layer-Upload ein Image-Manifest mit Referenzen auf die Image-Layer gepusht wird. Merkwürdig ist, dass die Layer-Chunks per base64-Encoding hochgeladen werden müssen, wodurch die Daten um etwa 33 % größer werden.Die Idee, die Anordnung von Dateipfaden als Mittel zur Endpoint-Steuerung zu nutzen, ist interessant.
Allerdings frage ich mich, wie der
Docker-Content-Digest-Header behandelt wird. Er ist zwar nicht verpflichtend, wird aber empfohlen in Antworten aufzunehmen, und viele Clients erwarten ihn und könnten Layer ohne diesen Header ablehnen. Außerdem könnte man Funktionen wie die referrers API der OCI-1.1-Spezifikation verpassen. Die Implementierung dürfte ziemlich knifflig sein.