Dependency Proxy
GitLab v19.4요약
Dependency Proxy는 DockerHub의 공개 레지스트리 이미지를 위한 풀스루 캐시입니다. 컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. 사용자 관점에서 GitLab 인스턴스는 docker login gitlab.com을 사용해 이미지를 pull할 때 상호작용하는 하나의 컨테이너 레지스트리일 뿐입니다
Dependency Proxy는 DockerHub의 공개 레지스트리 이미지를 위한 풀스루 캐시입니다. 이 문서에서는 GitLab에서 이 기능이 어떻게 구성되어 있는지 설명합니다.
컨테이너 레지스트리#
컨테이너 레지스트리용 Dependency Proxy는 원격 컨테이너 레지스트리의 대리자로 동작합니다. GitLab의 경우 원격 레지스트리는 공개 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에서 토큰을 받도록 안내합니다. 이 엔드포인트는 Rails 프로젝트 또는 웹 서비스라고도
부르는 gitlab-org/gitlab 프로젝트의 일부입니다.
사용자가 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 its header values
Workhorse->>Rails: Finalize the upload
end</code></pre></details></div>
정리 정책#
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 - 초기 인증 구현
- 매니페스트 캐싱 - 초기 매니페스트 캐싱 구현
- blob을 위한 Workhorse - 초기 Workhorse 구현
- 매니페스트를 위한 Workhorse - 매니페스트 캐시 로직을 Workhorse로 이동
- 배포 토큰 지원 - 인가 전면 업데이트
- SSO 지원 - 정책 확인 방식 변경