의존성 프록시

의존성 프록시는 DockerHub의 공용 레지스트리 이미지에 대한 풀 스루 캐시입니다. 이 문서는 GitLab에서 이 기능이 어떻게 구성되어 있는지를 설명합니다.

note
비공식 레지스트리 이미지에 대한 지원이 issue 331741에서 제안되었습니다.

컨테이너 레지스트리

컨테이너 레지스트리를 위한 의존성 프록시는 원격 컨테이너 레지스트리의 대리 역할을 합니다. 우리의 경우, 원격 레지스트리는 공용 DockerHub 레지스트리입니다.

$ docker
GitLab Dependency Proxy
DockerHub

사용자의 관점에서 GitLab 인스턴스는 그들이 docker login gitlab.com을 사용하여 이미지를 가져오기 위해 상호작용하는 컨테이너 레지스트리입니다.

docker login gitlab.com을 사용하면 Docker 클라이언트가 v2 API를 사용하여 요청을 수행합니다.

인증을 지원하기 위해 우리는 하나의 경로를 포함해야 합니다:

docker pull 요청을 지원하기 위해, 우리는 두 개의 추가 경로를 포함해야 합니다:

이 경로들은 gitlab-org/gitlab/config/routes/group.rb에서 정의되어 있습니다.

가장 간단한 형태에서 의존성 프록시는 세 가지 요청을 관리합니다:

  • 로그인 / JWT 반환
  • 매니페스트 가져오기
  • 블롭 가져오기

의존성 프록시에 대한 일반 요청 시퀀스는 다음과 같습니다:

ExternalRegistryGitLabClientExternalRegistryGitLabClientloop[이미지 레이어 요청]로그인? / 토큰 요청JWT이미지에 대한 매니페스트 요청JWT 요청JWT매니페스트 요청매니페스트 반환매니페스트 저장매니페스트 반환매니페스트에서 블롭 요청JWT 요청JWT블롭 요청블롭 반환블롭 저장블롭 반환

인증 및 권한 부여

Docker 클라이언트가 레지스트리에 인증할 때, 레지스트리는 클라이언트에게 JSON 웹 토큰(JWT)을 어디서 가져와야 하는지를 알려주고 이후 모든 요청에 사용하라고 합니다. 이를 통해 인증 서비스는 레지스트리와 별도의 애플리케이션으로 존재할 수 있습니다. 예를 들어, GitLab 컨테이너 레지스트리는 Docker 클라이언트에게 https://gitlab.com/jwt/auth에서 토큰을 가져오도록 안내합니다. 이 엔드포인트는 gitlab-org/gitlab 프로젝트의 일부로, Rails 프로젝트 또는 웹 서비스로도 알려져 있습니다.

사용자가 Docker 클라이언트를 사용하여 의존성 프록시에 로그인하려고 할 때, 우리는 JWT를 어디서 가져와야 하는지 알려줘야 합니다. 우리는 컨테이너 레지스트리와 함께 사용하는 것과 동일한 엔드포인트인 https://gitlab.com/jwt/auth를 사용할 수 있습니다. 그러나 우리의 경우, Docker 클라이언트에게 매개변수에서 service=dependency_proxy를 지정하도록 알려주어야 하며, 이를 통해 별도의 기본 서비스를 사용하여 토큰을 생성할 수 있습니다.

이 시퀀스 다이어그램은 의존성 프록시에 로그인하기 위한 요청 흐름을 보여줍니다.

GitLab (Dependency Proxy)Docker CLIGitLab (Dependency Proxy)Docker CLI사용자가 `docker login gitlab.com`을 시도하고 사용자 이름/비밀번호를 입력합니다.Authorization 헤더를 확인하고 없으면 401을 반환하고, 토큰이 존재하고 유효하면 200을 반환합니다.HTTP Basic Auth를 사용하여 Oauth 토큰 요청토큰이 반환됩니다.원래 요청이 다시 테스트됩니다.로그인 성공GET /v2/1401 Unauthorized with header "WWW-Authenticate": "Bearer realm=\"http://gitlab.com/jwt/auth\",service=\"registry.docker.io\""2GET /jwt/auth3200 OK (Bearer 토큰 포함)4GET /v2/ (이번에는 `Authorization: Bearer [token]` 헤더와 함께)5200 OK6

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

캐싱

Blob은 논리 없이 캐시된 아티팩트입니다. 우리는 이를 다이제스트로 캐시합니다. 새로운 blob에 대한 요청을 받으면, 요청된 다이제스트와 일치하는 blob이 있는지 확인하고, 있으면 반환합니다. 그렇지 않으면 외부 레지스트리에서 가져와서 캐시합니다.

매니페스트는 더 복잡합니다. 부분적으로는 DockerHub의 비율 제한 때문입니다.
매니페스트는 본질적으로 이미지를 생성하기 위한 레시피입니다. 특정 이미지를 생성하기 위한 blob 목록을 가지고 있습니다.
그래서 alpine:latestalpine:latest 이미지를 생성하는 데 필요한 blob을 지정하는 매니페스트가 연관되어 있습니다.
흥미로운 점은 alpine:latest가 시간이 지남에 따라 변경될 수 있기 때문에 매니페스트를 캐시하고 영원히 사용할 수 있다고 가정할 수 없습니다.
대신, 우리는 매니페스트의 다이제스트 즉 ETag를 확인해야 합니다. 요청에서 매니페스트에 대한 다이제스트가 포함되지 않는 경우가 많기 때문에 흥미롭습니다.
그렇다면 캐시된 매니페스트가 여전히 최신의 alpine:latest인지 어떻게 알 수 있을까요? DockerHub는 비율 제한에 포함되지 않는 무료 HEAD 요청을 허용합니다.
HEAD 요청은 매니페스트 다이제스트를 반환하므로 우리가 가진 것이 오래되었는지 여부를 알 수 있습니다.

이러한 지식을 바탕으로 매니페스트 요청을 관리하는 다음 논리를 구축했습니다:

매니페스트가 캐시되어 있습니다.
매니페스트가 캐시되어 있지 않습니다.
DB의 다이제스트와 일치
HEAD 요청 오류, 네트워크 실패, DockerHub 접근 불가
DB의 다이제스트와 일치하지 않음
매니페스트 요청 수신
Docker 매니페스트 HEAD 요청
Docker 매니페스트 GET 요청
캐시에서 매니페스트 가져오기
매니페스트를 캐시에 저장하고, 다이제스트를 데이터베이스에 저장
매니페스트 반환

파일 처리를 위한 워크호스

파일 업로드 및 캐시 관리는 워크호스에서 진행됩니다. 이것은 우리가 Dependency Proxy를 위해 추가한 POST 라우트를 설명합니다.

send_dependency
메서드는 외부 레지스트리에서 이전에 가져온 JWT를 포함하여 워크호스에 요청을 합니다. 그러면 워크호스는 해당 토큰을 사용하여
사용자가 원래 요청한 매니페스트나 blob을 요청할 수 있습니다. 워크호스 코드는
workhorse/internal/dependencyproxy/dependencyproxy.go에 있습니다.

모든 것을 종합하면, 이미지 파일 요청의 순서는 다음과 같습니다:

Object StorageExternal RegistryRailsWorkhorseClientObject StorageExternal RegistryRailsWorkhorseClientalt[캐시에 있음][캐시에 없음]GET /v2/*group_id/dependency_proxy/containers/*image/manifests/*tagGET /v2/*group_id/dependency_proxy/containers/*image/manifests/*tagDB 확인. 매니페스트가 캐시에 저장되어 있습니까?send-url 주입기로 응답클라이언트에게 파일 전송인증 토큰 및 업스트림 레지스트리의 매니페스트 다운로드 URL 생성send-dependency 주입기로 응답매니페스트 요청매니페스트 다운로드GET /v2/*group_id/dependency_proxy/containers/*image/manifest/*tag/authorize업로드 지침으로 응답원래 헤더와 함께 클라이언트에게 매니페스트 파일 전송일부 헤더 값을 포함하여 매니페스트 파일 저장업로드 완료

정리 정책

의존성 프록시의 정리 정책은 TTL(시간 제한) 정책으로 작동합니다.

이 정책은 사용자가 파일이 읽히지 않은 경우 캐시된 상태로 허용되는 날짜 수를 설정할 수 있게 해줍니다.

Blob을 해당 이미지와 연결하는 방법이 없기 때문에(이를 위해서는 컨테이너 레지스트리 팀이 구축한 메타데이터 데이터베이스를 만들어야 합니다),

“이 blob이 90일 동안 가져오지 않았다면 삭제하세요”와 같은 규칙을 설정할 수 있습니다.

이는 지속적으로 가져오는 파일은 캐시에서 제거되지 않음을 의미합니다.

하지만 예를 들어, alpine:latest가 변경되고 그 기반 blob 중 하나가 더 이상 사용되지 않게 되면,

결국 가져오기가 중지되었기 때문에 정리됩니다.

우리는 주어진 dependency_proxy_blob 또는 dependency_proxy_manifest가 마지막으로 가져온 시간을 추적하기 위해 read_at 속성을 사용합니다.

이들은 DependencyProxy::CleanupDependencyProxyWorker라는 크론 워커를 사용하여 작동하며,

두 개의 제한 용량 워커를 시작합니다: 하나는 blobs를 삭제하고,

다른 하나는 manifests를 삭제합니다. 용량은 애플리케이션 설정에서 설정됩니다.

역사적 참조 링크