依存関係プロキシ

依存プロキシはDockerHubの公開レジストリイメージのプルスルーキャッシュです。このドキュメントでは、この機能がGitLabでどのように構築されているかを説明します。

note
非公開レジストリイメージのサポートはイシュー331741で提案されています。

コンテナレジストリ

コンテナレジストリの依存プロキシは、リモートコンテナレジストリの代用となります。この場合、リモートレジストリは公開 DockerHub レジストリです。

flowchart TD id1([$ docker]) --> id2([GitLab Dependency Proxy]) id2 --> id3([DockerHub])

ユーザーから見れば、GitLab インスタンスは単なるコンテナレジストリに過ぎず、イメージのプルにはdocker login gitlab.com

docker login gitlab.com を使うと、Dockerクライアントはリクエストを行うためにv2 APIを使います。

認証をサポートするために、1つのルートを含める必要があります:

docker pull リクエストをサポートするには、さらに2つのルートを含める必要があります:

これらのルートはgitlab-org/gitlab/config/routes/group.rbで定義されています。

最も単純な形では、依存プロキシは3つのリクエストを管理します:

  • ログイン/JWTのリターン
  • マニフェストのフェッチ
  • ブロブのフェッチ

依存プロキシの一般的なリクエストシーケンスは次のようになります:

sequenceDiagram Client->>+GitLab: Login? / request token GitLab->>+Client: JWT Client->>+GitLab: request a manifest for an image GitLab->>+ExternalRegistry: request JWT ExternalRegistry->>+GitLab : JWT GitLab->>+ExternalRegistry : request manifest ExternalRegistry->>+GitLab : return manifest GitLab->>+GitLab : store manifest GitLab->>+Client : return manifest loop request image layers Client->>+GitLab: request a blob from the manifest GitLab->>+ExternalRegistry: request JWT ExternalRegistry->>+GitLab : JWT GitLab->>+ExternalRegistry : request blob ExternalRegistry->>+GitLab : return blob GitLab->>+GitLab : store blob GitLab->>+Client : return blob end

認証と作成者

Dockerクライアントがレジストリで認証するとき、レジストリはクライアントにJSON Web Token(JWT) を取得する場所と、それ以降のすべてのリクエストにそれを使用するように指示します。これにより、認証サービスをレジストリとは別のアプリケーションで行うことができます。たとえば GitLab コンテナレジストリは、Docker クライアントにhttps://gitlab.com/jwt/auth からトークンを取得するように指示します。このエンドポイントはgitlab-org/gitlab プロジェクトの一部で、Rails プロジェクトや Web サービスとしても知られています。

ユーザーがDockerクライアントで依存プロキシにサインインしようとするとき、JWTをどこで取得するかを伝えなければなりません。コンテナレジストリで使っているのと同じエンドポイントを使うことができます:https://gitlab.com/jwt/auth 。しかし私たちのケースでは、Dockerクライアントにパラメータでservice=dependency_proxy を指定するように指示し、トークンを生成するために別の基礎となるサービスを使用できるようにしています。

このシーケンス図は、依存プロキシにログインするためのリクエストフローを示しています。

sequenceDiagram autonumber participant C as Docker CLI participant R as GitLab (Dependency Proxy) Note right of C: User tries `docker login gitlab.com` and enters username/password C->>R: GET /v2/ Note left of R: Check for Authorization header, return 401 if none, return 200 if token exists and is valid R->>C: 401 Unauthorized with header "WWW-Authenticate": "Bearer realm=\"http://gitlab.com/jwt/auth\",service=\"registry.docker.io\"" Note right of C: Request Oauth token using HTTP Basic Auth C->>R: GET /jwt/auth Note left of R: Token is returned R->>C: 200 OK (with Bearer token included) Note right of C: original request is tested again C->>R: GET /v2/ (this time with `Authorization: Bearer [token]` header) Note right of C: Login Succeeded R->>C: 200 OK

依存プロキシは、UI (ApplicationController) や API (ApiGuard) が管理する認証とは別に、独自の認証サービスを使います。サービスがJWTを作成すると、DependencyProxy::ApplicationController は残りのリクエストの認証と認可を管理します。GitLab::Auth::Result を使ってユーザーを管理し、GitHttpClientControllerの Git クライアントリクエストで実装されている認証に似ています。

キャッシュ

ブロブはキャッシュされたアーティファクトで、ロジックはありません。ダイジェストでキャッシュします。新しいblobのリクエストを受け取ると、リクエストされたダイジェストを持つblobがあるかどうかをチェックし、それを返します。そうでない場合は、外部レジストリから取得してキャッシュします。

マニフェストは、DockerHubのレート制限のせいもあり、より複雑です。マニフェストは基本的にイメージを作成するためのレシピです。マニフェストには、特定のイメージを作成するためのblobのリストがあります。そのため、イメージのalpine:latest 作成に必要なblobを指定するマニフェストが関連付けられて alpine:latestいます。興味深いのは、マニフェストはalpine:latest 時間の経過とともに変更される可能性が alpine:latestあるということです。alpine:latest その代わりに、マニフェストのダイジェスト(ETag)をチェックする必要があります。マニフェストのリクエストにはダイジェストが含まれていないことが多いので、これは興味深いことです。では、私たちがキャッシュしたマニフェストがまだ最新か alpine:latestどうかを知るにはどうすればよいのでしょうか?DockerHubはレート制限にカウントされない無料のHEADリクエストを許可しています。HEADリクエストはマニフェストのダイジェストを返すので、今あるものが古いかどうかを知ることができます。

この知識をもとに、マニフェストリクエストを管理するための以下のロジックを構築しました:

graph TD A[Receive manifest request] --> | We have the manifest cached.| B{Docker manifest HEAD request} A --> | We do not have manifest cached.| C{Docker manifest GET request} B --> | Digest matches the one in the DB | D[Fetch manifest from cache] B --> | HEAD request error, network failure, cannot reach DockerHub | D[Fetch manifest from cache] B --> | Digest does not match the one in DB | C C --> E[Save manifest to cache, save digest to database] D --> F E --> F[Return manifest]

ファイル処理の Workhorse

ファイルのアップロードとキャッシュの管理はWorkhorseで行われます。依存プロキシに追加されたPOST ルート はこのためです。

send_dependency メソッドは、外部レジストリから以前に取得した JWT を含むリクエストを Workhorse に行います。Workhorse はそのトークンを使用して、ユーザーが最初にリクエストしたマニフェストまたは blob をリクエストできます。Workhorse コードはworkhorse/internal/dependencyproxy/dependencyproxy.goにあります。

すべてをまとめると、画像ファイルをリクエストするシーケンスは次のようになります:

sequenceDiagram Client->>Workhorse: GET /v2/*group_id/dependency_proxy/containers/*image/manifests/*tag Workhorse->>Rails: GET /v2/*group_id/dependency_proxy/containers/*image/manifests/*tag Rails->>Rails: Check DB. Is manifest persisted in cache? alt In Cache Rails->>Workhorse: Respond with send-url injector Workhorse->>Client: Send the file to the client else Not In Cache Rails->>Rails: Generate auth token and download URL for the manifest in upstream registry Rails->>Workhorse: Respond with send-dependency injector Workhorse->>External Registry: Request the manifest External Registry->>Workhorse: Download the manifest Workhorse->>Rails: GET /v2/*group_id/dependency_proxy/containers/*image/manifest/*tag/authorize Rails->>Workhorse: Respond with upload instructions Workhorse->>Client: Send the manifest file to the client with original headers Workhorse->>Object Storage: Save the manifest file with some of it's header values Workhorse->>Rails: Finalize the upload end

クリーンアップポリシー

依存プロキシのクリーンアップポリシーは、time-to-live ポリシーとして動作します。ユーザーによって、未読のファイルがキャッシュされたままになる日数を設定できます。BLOBを所属するイメージに関連付ける方法はないため(これを行うには、コンテナレジストリの人々が構築したメタデータデータベースを構築する必要があります)、「このBLOBが90日間プルされていない場合は削除する」といったルールを設定できます。これは、継続的にプルされているファイルはキャッシュから削除されないことを意味しますが、例えば、alpine:latest が変更され、基礎となるブロブの1つが使用されなくなった場合、プルされなくなったため、最終的にクリーンアップされます。read_at 属性を使用して、指定されたdependency_proxy_blob またはdependency_proxy_manifest が最後にプルされた時間を追跡します。

これらはDependencyProxy::CleanupDependencyProxyWorkerというcronワーカーを使って動作します。容量はアプリケーション設定で設定します。