S3をコンテナレジストリとして使う
(ochagavia.nl)- コンテナイメージは通常、専用レジストリにアップロードするが、S3バケットを HTTPで公開し、ファイルを決められたパスに配置すれば、
docker pullの対象のように使える - カスタムイメージビルダーで pull 可能なイメージを 数秒以内に作る工程がボトルネックになっており、レイヤーの push 時間が大きな割合を占めていた
- 198 MiB レイヤーのアップロードベンチマークでは、ECRは24〜28 MiB/s、S3は115〜190 MiB/sと測定され、S3が最大 8倍高速だった
- この差は、S3が単一レイヤーのチャンクを 並列アップロードできる一方で、OCI Distribution SpecベースのECRはチャンクを順次処理しなければならないことから生じる
- この方式は、
docker push、イメージの妥当性検証、セキュリティスキャン、プライベートリポジトリのアクセス制御といった レジストリ機能を代替できないため、実験的な最適化と見るべき
S3バケットで docker pull できる条件
- S3をコンテナレジストリのように使うには、バケットを HTTPで公開し、Dockerが想定するパスにイメージファイルをアップロードする必要がある
- 条件が合えば、通常のレジストリのように
docker pullでイメージを取得できる - デモイメージは cowsay を実行し、バケットURLをイメージ名のように使って
docker run --rm .../cowsayで動作する - デモには Cloudflare R2 が使われている
- 無料の egress が選定理由
- R2とS3はAPI互換性があるため、AWS SDKでR2にイメージをアップロードしている
なぜ専用レジストリではなくS3を検討したのか
- コンテナイメージは通常、DockerHub、GitHub Container Registry、ECR のような 専用レジストリでホストする
- カスタムイメージビルダーの目標は、要件から pull 可能なイメージまで数秒以内に到達すること
- AWS環境ではECRが最も簡単な選択肢だが、実際のアップロード速度では S3とECRの差が大きく現れる
- 性能最適化のためコードに実行トレース(trace)を入れたところ、レイヤーをコンテナレジストリに push する時間が大きなボトルネックだと確認された
198 MiB レイヤーのアップロード結果
- 小規模なベンチマークでは、198 MiB レイヤーをECRとS3にそれぞれアップロードし、所要時間とスループットを比較した
- 観測された速度は次のとおり
- ECR: 最小 24 MiB/s、8.2秒
- ECR: 最大 28 MiB/s、7.0秒
- S3: 最小 115 MiB/s、1.7秒
- S3: 最大 190 MiB/s、1.0秒
- 結果としてS3はECRより最大 8倍高速な水準
- 実験コードはAWS上で実行され、S3とECRはVPC経由で内部接続されており、パブリックインターネットを経由していない
- この条件は、可能な限り良好なレイテンシと帯域幅を提供する
並列チャンクアップロードが生んだ速度差
- S3アップロードでは、単一レイヤーのチャンクを 並列にアップロードできる
- 十分な帯域幅があれば、並列チャンクアップロードによってスループットが大きく向上する
- AWSドキュメントでも、帯域幅の使用を最大化するために並列チャンクアップロードを推奨している
- ECRは OCI Distribution Spec を実装している
- この仕様は、複数のレジストリで
docker pullとdocker pushが動作するようにする標準 - レイヤーの push は順次進める必要があり、チャンクアップロードを行う場合でも、前のチャンクが終わってから次のチャンクに進む必要がある
- この仕様は、複数のレジストリで
- S3でも順次アップロードをテストすると、スループットは ECRと同程度まで下がる
docker pull の実際のリクエスト構造
docker pullの内部リクエストは、複数の HEAD と GET リクエストで構成される- 例の流れは次のとおり
- イメージマニフェストの存在確認:
HEAD /v2/my-image/manifests/latest - イメージマニフェストのダウンロード:
GET /v2/my-image/manifests/latest - マニフェストハッシュで再度ダウンロード:
GET /v2/my-image/manifests/sha256:... - イメージメタデータ blob のダウンロード:
GET /v2/my-image/blobs/sha256:... - イメージレイヤー blob のダウンロード:
GET /v2/my-image/blobs/sha256:...
- イメージマニフェストの存在確認:
- 結局、
docker pullは必要なファイルをHTTPでダウンロードする処理に近い - 静的ファイルサーバーが必要なファイルを想定されたパスに置き、各リクエストに合った Content-Type ヘッダーを設定すれば、コンテナイメージを pull できる
- S3バケットはこの2つの条件を満たせるため、慎重に構成すればコンテナレジストリのように動作する
実験的アプローチの限界
- この方式はまだ 実験的であり、さらに調査する前に強い結論を出すのは難しい
- S3は厳密な意味でのコンテナレジストリではない
docker pushはできないdocker pullできるのは、HTTPリクエスト構造と静的ファイル提供方式がうまく噛み合った結果
- 既存のコンテナレジストリは、バケットにファイルを置く方式より多くの機能を提供する
- 標準の push 方式でアップロードされたイメージが実際に有効だと信頼できる
- レイヤーに対する自動セキュリティスキャンと警告を提供できる
- プライベートリポジトリへのアクセス権限をネイティブに指定できる
- 期待どおりに動作するなら、パブリックコンテナイメージをCloudflare R2でホストする方式も可能になるかもしれない
1件のコメント
Hacker Newsの意見
OCI Distribution Spec は、よく設計された仕様として読めないのが残念
仕様上、レイヤーのプッシュは逐次的に行う必要があるため、チャンクでアップロードしても各チャンクが完了しないと次に進めない。DockerHubとGHCRでテストした限りでは、チャンクアップロード自体も結局壊れていて、クライアントは各blob/レイヤーを丸ごとアップロードすることが多い。仕様では、RFC7233形式に合わない
Content-Range値の形式も推奨しているもちろんblob単位の並列性はあるが、blob内部の並列性はない。また、タグ一覧のページネーション標準化の機会を逃したのも不満。標準から関連文言が誤って削除されたことで [1]、レジストリごとにばらばらに実装されることになった
[1] https://github.com/opencontainers/distribution-spec/issues/4...
とはいえ、完全に非難したいわけではない。実際に革命的で、Linux名前空間の利用を以前よりはるかに簡単にし、世の中をより良い方向に変えた。ただ、常に技術的完成度よりもユーザー体験を優先してきたし、それ自体が必ずしも悪いわけでもない。PerlやFTPでやり取りするCSVで高価な問題を解いている退屈な会社が多いように、退屈だったり、時には悪い技術であっても、良いパッケージとして提供すれば大きな価値がある
それでも時々、今よりずっと良くできたはずなのにと思って苦く感じる
たとえば
.dkr.ecr..amazonaws.com/foo/bar/baz:tagのような形式は使えず、フラットな保存しかできないため、イメージ名やタグが過度に長くなる。理論上はTerraformでECRリポジトリオブジェクトを作成して似たようにまねることはできるが、結果のイメージパスが動的なパイプラインではいまいち。CIパイプラインのIAMロールに不便なほど多くの権限を与える必要があり、中央のTerraformリポジトリの外でAWSリソースが管理されるのも気に入らない[1] https://stackoverflow.com/questions/64232268/storing-images-...
CloudflareがR2を使うコンテナレジストリサーバーをオープンソースとして公開したことがある
使ったことがある人がいるのか気になる
[1]: https://github.com/cloudflare/serverless-registry
用途によっては大きな問題ではないかもしれないが、別の場合には即座に候補から外れる条件になり得る
投稿者です。OCI 仕様で、なぜレイヤーのプッシュは逐次的でなければならないのか、知っている人がいれば教えてほしいです
単なる歴史的な偶然なのか、隠れた理由があるのか気になります。明確にすると、複数のレイヤーは当然並列にプッシュできますが、ここで言っているのは単一レイヤーの内容を逐次的にプッシュしなければならない部分です
N+Timeout後に完了していないアップロードであることが明確なので削除すればよい、ということです部分アップロードをどう扱うかという実装の詳細を単純化しているわけです。そうでなければ、各チャンクの終わりごとに他のチャンクがすべてあるか確認して完了処理をしなければなりません。ただしこれは実装の詳細であり、意味のある、あるいは意図された設計だったのかは疑わしいです。S3 方式はうまく機能しそうですし、以前、大きなイメージを配布していた会社で似たことをしたことがあります。月あたり GB あたり 0.10 ドルがかなり積み上がりました
ECR の追加機能は失いますが、個人的にはそれらの機能はかなり限定的だと思っています
クライアントが blob アップロードを完了するとき、blob 全体のダイジェストを提供する必要があります。この要件は、サーバーが受け取ったバイト列の完全性を検証できるようにするためのものに見えます。サーバーが最後の HTTP リクエストでようやくダイジェスト検査を開始するなら、それ以前の HTTP リクエストで既にストレージに書き込んだ blob の内容をすべて読み直さなければなりません。大きなレイヤーではこの遅延は許容しにくいかもしれません。特定のクライアント要件のために、150GiB の blob まで動作することを検証しました
代わりに実装では、リクエストシーケンス全体にわたってダイジェスト計算を継続します。blob データをチャンク単位で受け取りながら、同時にダイジェスト計算と blob ストレージへのストリーミングを行います。各リクエストの間には、ダイジェスト計算の状態を
Locationヘッダーでクライアントに返すアップロード URL の中にシリアライズします。おおよそこのコードで処理されています: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...リファレンス実装も同じアプローチを使っていると理解しています。ダイジェスト計算は逐次的にしかできないため、アップロードも逐次的に進める必要があります
[1] https://github.com/sapcc/keppel
[2] https://github.com/distribution/distribution
GitHub リポジトリのリンクだけでもあるとうれしいです。背景としては、
$PROGRAMMING_LANGUAGEの中からプログラム的に OCI イメージを作る方法を探している、あるいは自分で実装しようと考えているためです。Buildah のようにしたいのですが、コマンドラインインターフェースではなく、実際のプログラミング言語向けの API が欲しいです。もちろん Buildah をサブプロセスとして呼び出すこともできますが、やや面倒ですし、Buildah の内部状態とのやり取りやクリーンアップの問題にも気を配る必要があります。また、現在 Buildah は Mac もサポートしていません以前 Docker に並列プッシュを追加したような気がしますが、pull と push を混同しているのかもしれません。後から見ると、私の作業は最終的なプッシュではなく、検査の並列化の方でした。レイヤーがどのレイヤーの「上に」乗るのかを明示するなら、参照先の ID が既に存在している必要があるので、その可能性もあります
かなり面白いユースケースです
個人的には Nexus をそのまま使っています。十分うまく動作し、OCI イメージから apt パッケージ、カスタム Maven、NuGet、npm リポジトリのようなものまでサポートしています。ただし設定とリソース使用量は少し厄介で、特にクリーンアップポリシー周りがそうです: https://www.sonatype.com/products/sonatype-nexus-repository
それでも、
docker pullが「単に」HEADとGETリクエストのまとまりである点は本当に良いです。昔からうまく動いていたものをそのまま使い、無駄に複雑にしない、こうした常識的な判断をもっと多くの技術で見たいです。認証とクリーンアップ機能まで備えた単純なコンテナストレージがもっと多くないのは意外です。Nexus と Harbor は、実際に使うにはどちらもかなり複雑ですこのスレッドで誰も言及していないのが意外です
CNCF の Distribution、旧 Docker Registry には、S3 から取得する CloudFront 署名付き URL でレジストリを支える機能が含まれています [1]
https://distribution.github.io/distribution/storage-drivers/...
https://github.com/distribution/distribution は何が問題なのか気になります
この方式はかなり高くつきそうに見えるので、記事でコストにも触れてくれていたらよかったと思う。S3 と R2 の両方が気になる
無料のインターネット向け送信トラフィックのコストは同じだが、公開 ECR リポジトリには AWS 内部利用向けの送信トラフィックを無料にする例外がある
GET/PUTコスト、帯域幅コストは AWS のウェブサイトで確認できる: https://aws.amazon.com/s3/pricing/開発ツール以外では Docker をあまり使わないが、なぜプライベートコンテナレジストリが存在しなければならないのか理解したことがない
ただのレントシーキングに見える。自分で管理するイメージファイルのようなものを作って好きに使うのと比べて、実際にどんな利点があるのか気になる
docker saveとdocker importを使えばいいdocker save alpine:3.19 > alpine.tardocker load < alpine.tarただし、その tar ファイルを管理しなければならず、すべてのシステムがその場所とアクセス方法を知っている必要がある。あるいは車輪の再発明をせず、Docker がすでに提供している方式を使えばいい
そのため、自分でレジストリを作るか、誰かにお金を払って任せる必要がある。もちろん開発用途だけでイメージを使うなら、こうしたことはすべて意味がなく、開発マシンにイメージを保存しておけばよい
以前のバージョンがすべてあり、複数の利用者が簡単にアクセスできる中央リポジトリが必要だから。アプリをビルドしたあと、実行され得るすべての場所にいちいち押し込みたくない。一度ビルドして中央リポジトリにプッシュし、すべての場所がそのリポジトリを参照するようにすればよい
プライベートリポジトリのホスティングに必ずしもお金を払う必要はない。自分でホストできるツールはたくさんある
環境ごとに Terraform、Bicep、Pulumi ですべて構成できる
ECR は実際、イメージレイヤーを複数のパートに分けてアップロードできるように設計されているように見える
関連する ECR API には、各イメージレイヤーのアップロード開始時に呼び出す
InitiateLayerUpload API、各レイヤーチャンクごとに呼び出すUploadLayerPart API(最大 20MB)、レイヤーのアップロード後にイメージレイヤー参照を含むイメージマニフェストをプッシュするPutImage APIがある。妙なのは、レイヤーチャンクをbase64 エンコードしてアップロードしなければならず、データが約 33% 増えることファイルパスの配置をエンドポイント制御の手段として使うアイデアは興味深い
ただし、
Docker-Content-Digestヘッダーをどう扱うのかが気になる。必須ではないがレスポンスに含めることが推奨されており、多くのクライアントがこれを期待し、ヘッダーのないレイヤーを拒否する可能性がある。また、OCI 1.1 仕様のreferrers APIのような機能は取りこぼすかもしれない。実装はかなり厄介そう