InfoGrab DocsInfoGrab Docs

Dependency Proxy

요약

Dependency Proxy는 DockerHub의 퍼블릭 레지스트리 이미지를 위한 풀-스루 캐시입니다. 컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. flowchart TD id1([$ docker]) --> id2([GitLab Dependency Proxy]) id2 --> id3([DockerHub])

Dependency Proxy는 DockerHub의 퍼블릭 레지스트리 이미지를 위한 풀-스루 캐시입니다. 이 문서는 GitLab에서 이 기능이 어떻게 구성되는지 설명합니다.

컨테이너 레지스트리#

컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. 여기서 원격 레지스트리는 퍼블릭 DockerHub 레지스트리입니다.

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

사용자 관점에서, GitLab 인스턴스는 docker login gitlab.com을 사용하여 이미지를 pull하기 위해 상호작용하는 단순한 컨테이너 레지스트리입니다.

docker login gitlab.com을 사용하면 Docker 클라이언트는 v2 API를 사용하여 요청을 보냅니다.

인증을 지원하려면 다음 라우트를 반드시 포함해야 합니다:

docker pull 요청을 지원하려면 두 개의 추가 라우트를 포함해야 합니다:

이 라우트들은 gitlab-org/gitlab/config/routes/group.rb에 정의되어 있습니다.

가장 단순한 형태로, Dependency Proxy는 세 가지 요청을 처리합니다:

  • 로그인 / JWT 반환

  • 매니페스트 가져오기

  • blob 가져오기

다음은 Dependency Proxy의 일반적인 요청 시퀀스입니다:

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 프로젝트 또는 웹 서비스라고도 함)의 일부입니다.

사용자가 Docker 클라이언트로 dependency proxy에 로그인하려 할 때, JWT를 받을 위치를 알려줘야 합니다. 컨테이너 레지스트리에서 사용하는 것과 동일한 엔드포인트(https://gitlab.com/jwt/auth)를 사용할 수 있습니다. 하지만 이 경우에는 Docker 클라이언트가 파라미터에 service=dependency_proxy를 지정하도록 하여 토큰 생성에 별도의 기반 서비스를 사용할 수 있게 합니다.

다음 시퀀스 다이어그램은 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

dependency proxy는 UI(ApplicationController)와 API(ApiGuard)에서 관리하는 인증과 별도의 자체 인증 서비스를 사용합니다. 서비스가 JWT를 생성하면, DependencyProxy::ApplicationController가 나머지 요청에 대한 인증 및 인가를 관리합니다. GitLab::Auth::Result를 사용하여 사용자를 관리하며, GitHttpClientController에서 Git 클라이언트 요청에 구현된 인증과 유사합니다.

캐싱#

blob은 별도의 로직 없이 캐시되는 아티팩트입니다. 다이제스트를 기준으로 캐시합니다. 새 blob에 대한 요청이 오면, 요청된 다이제스트를 가진 blob이 있는지 확인하고 반환합니다. 없으면 외부 레지스트리에서 가져와 캐시합니다.

매니페스트는 DockerHub의 속도 제한으로 인해 더 복잡합니다. 매니페스트는 본질적으로 이미지 생성을 위한 레시피입니다. 특정 이미지를 생성하기 위한 blob 목록을 가집니다. 예를 들어 alpine:latest에는 alpine:latest 이미지를 생성하는 데 필요한 blob을 지정하는 매니페스트가 연결되어 있습니다. 흥미로운 점은 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에서 처리됩니다. 이것이 Dependency Proxy에 추가적인 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

정리 정책#

Dependency Proxy의 정리 정책은 TTL(Time-To-Live) 정책으로 동작합니다. 사용자는 파일을 읽지 않은 상태에서 캐시에 남아 있을 수 있는 일수를 설정할 수 있습니다. blob을 해당 blob이 속하는 이미지와 연결할 방법이 없기 때문에(이를 위해서는 컨테이너 레지스트리 팀이 구축한 메타데이터 데이터베이스가 필요합니다), "이 blob이 90일 동안 pull되지 않으면 삭제"와 같은 규칙을 설정할 수 있습니다. 이는 지속적으로 pull되는 파일은 캐시에서 제거되지 않지만, 예를 들어 alpine:latest가 변경되어 기반 blob 중 하나가 더 이상 사용되지 않으면 결국 pull이 중단되므로 정리된다는 것을 의미합니다. 특정 dependency_proxy_blob 또는 dependency_proxy_manifest가 마지막으로 pull된 시간을 추적하기 위해 read_at 속성을 사용합니다.

이는 두 개의 제한된 용량 워커를 시작하는 cron 워커 DependencyProxy::CleanupDependencyProxyWorker를 사용하여 동작합니다. 하나는 blob을 삭제하고, 다른 하나는 매니페스트를 삭제합니다. 용량은 애플리케이션 설정에서 설정됩니다.

히스토리 참조 링크#

Dependency Proxy

GitLab v19.1
원문 보기
요약

Dependency Proxy는 DockerHub의 퍼블릭 레지스트리 이미지를 위한 풀-스루 캐시입니다. 컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. flowchart TD id1([$ docker]) --> id2([GitLab Dependency Proxy]) id2 --> id3([DockerHub])

Dependency Proxy는 DockerHub의 퍼블릭 레지스트리 이미지를 위한 풀-스루 캐시입니다. 이 문서는 GitLab에서 이 기능이 어떻게 구성되는지 설명합니다.

컨테이너 레지스트리#

컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. 여기서 원격 레지스트리는 퍼블릭 DockerHub 레지스트리입니다.

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

사용자 관점에서, GitLab 인스턴스는 docker login gitlab.com을 사용하여 이미지를 pull하기 위해 상호작용하는 단순한 컨테이너 레지스트리입니다.

docker login gitlab.com을 사용하면 Docker 클라이언트는 v2 API를 사용하여 요청을 보냅니다.

인증을 지원하려면 다음 라우트를 반드시 포함해야 합니다:

docker pull 요청을 지원하려면 두 개의 추가 라우트를 포함해야 합니다:

이 라우트들은 gitlab-org/gitlab/config/routes/group.rb에 정의되어 있습니다.

가장 단순한 형태로, Dependency Proxy는 세 가지 요청을 처리합니다:

  • 로그인 / JWT 반환

  • 매니페스트 가져오기

  • blob 가져오기

다음은 Dependency Proxy의 일반적인 요청 시퀀스입니다:

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 프로젝트 또는 웹 서비스라고도 함)의 일부입니다.

사용자가 Docker 클라이언트로 dependency proxy에 로그인하려 할 때, JWT를 받을 위치를 알려줘야 합니다. 컨테이너 레지스트리에서 사용하는 것과 동일한 엔드포인트(https://gitlab.com/jwt/auth)를 사용할 수 있습니다. 하지만 이 경우에는 Docker 클라이언트가 파라미터에 service=dependency_proxy를 지정하도록 하여 토큰 생성에 별도의 기반 서비스를 사용할 수 있게 합니다.

다음 시퀀스 다이어그램은 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

dependency proxy는 UI(ApplicationController)와 API(ApiGuard)에서 관리하는 인증과 별도의 자체 인증 서비스를 사용합니다. 서비스가 JWT를 생성하면, DependencyProxy::ApplicationController가 나머지 요청에 대한 인증 및 인가를 관리합니다. GitLab::Auth::Result를 사용하여 사용자를 관리하며, GitHttpClientController에서 Git 클라이언트 요청에 구현된 인증과 유사합니다.

캐싱#

blob은 별도의 로직 없이 캐시되는 아티팩트입니다. 다이제스트를 기준으로 캐시합니다. 새 blob에 대한 요청이 오면, 요청된 다이제스트를 가진 blob이 있는지 확인하고 반환합니다. 없으면 외부 레지스트리에서 가져와 캐시합니다.

매니페스트는 DockerHub의 속도 제한으로 인해 더 복잡합니다. 매니페스트는 본질적으로 이미지 생성을 위한 레시피입니다. 특정 이미지를 생성하기 위한 blob 목록을 가집니다. 예를 들어 alpine:latest에는 alpine:latest 이미지를 생성하는 데 필요한 blob을 지정하는 매니페스트가 연결되어 있습니다. 흥미로운 점은 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에서 처리됩니다. 이것이 Dependency Proxy에 추가적인 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

정리 정책#

Dependency Proxy의 정리 정책은 TTL(Time-To-Live) 정책으로 동작합니다. 사용자는 파일을 읽지 않은 상태에서 캐시에 남아 있을 수 있는 일수를 설정할 수 있습니다. blob을 해당 blob이 속하는 이미지와 연결할 방법이 없기 때문에(이를 위해서는 컨테이너 레지스트리 팀이 구축한 메타데이터 데이터베이스가 필요합니다), "이 blob이 90일 동안 pull되지 않으면 삭제"와 같은 규칙을 설정할 수 있습니다. 이는 지속적으로 pull되는 파일은 캐시에서 제거되지 않지만, 예를 들어 alpine:latest가 변경되어 기반 blob 중 하나가 더 이상 사용되지 않으면 결국 pull이 중단되므로 정리된다는 것을 의미합니다. 특정 dependency_proxy_blob 또는 dependency_proxy_manifest가 마지막으로 pull된 시간을 추적하기 위해 read_at 속성을 사용합니다.

이는 두 개의 제한된 용량 워커를 시작하는 cron 워커 DependencyProxy::CleanupDependencyProxyWorker를 사용하여 동작합니다. 하나는 blob을 삭제하고, 다른 하나는 매니페스트를 삭제합니다. 용량은 애플리케이션 설정에서 설정됩니다.

히스토리 참조 링크#