의존성 프록시

의존성 프록시는 DockerHub의 공개 레지스트리 이미지를 위한 pull-through-cache입니다. 본 문서에서는 GitLab에서 이 기능이 구성되는 방법에 대해 설명합니다.

참고: 사설 레지스트리 이미지 지원은 이슈 331741에서 제안되었습니다.

컨테이너 레지스트리

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

flowchart TD id1([$ docker]) --> id2([GitLab 의존성 프록시]) id2 --> id3([DockerHub])

사용자 관점에서는 GitLab 인스턴스가 단순히 docker login gitlab.com을 사용하여 이미지를 끌어오는 컨테이너 레지스트리로 작용합니다.

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

인증을 지원하기 위해 한 경로를 포함해야 합니다:

docker pull 요청을 지원하려면 두 가지 추가 경로를 포함해야 합니다:

이러한 경로는 gitlab-org/gitlab/config/routes/group.rb에서 정의됩니다.

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

  • 로그인 / JWT 반환
  • 매니페스트 검색
  • 블롭 검색

의존성 프록시의 일반적인 요청 순서는 다음과 같습니다:

sequenceDiagram Client->>+GitLab: 로그인? / 토큰 요청 GitLab->>+Client: JWT Client->>+GitLab: 이미지에 대한 매니페스트 요청 GitLab->>+ExternalRegistry: JWT 요청 ExternalRegistry->>+GitLab : JWT GitLab->>+ExternalRegistry : 매니페스트 요청 ExternalRegistry->>+GitLab : 매니페스트 반환 GitLab->>+GitLab : 매니페스트 저장 GitLab->>+Client : 매니페스트 반환 loop 이미지 레이어 요청 Client->>+GitLab: 매니페스트에서 블롭 요청 GitLab->>+ExternalRegistry: JWT 요청 ExternalRegistry->>+GitLab : JWT GitLab->>+ExternalRegistry : 블롭 요청 ExternalRegistry->>+GitLab : 블롭 반환 GitLab->>+GitLab : 블롭 저장 GitLab->>+Client : 블록 반환 end

인증 및 권한 부여

Docker 클라이언트가 레지스트리와 인증을 수행할 때, 레지스트리에서 클라이언트에게 JSON 웹 토큰(JWT)의 획득처와 이를 후속 요청에 사용하도록 알려줍니다. 이를 통해 인증 서비스는 레지스트리와는 별도의 애플리케이션에서 운영될 수 있습니다. 예를 들어, GitLab 컨테이너 레지스트리는 Docker 클라이언트에게 https://gitlab.com/jwt/auth에서 토큰을 가져오라고 지시합니다. 이 엔드포인트는 gitlab-org/gitlab 프로젝트, 즉 rails 프로젝트 또는 웹 서비스의 일부입니다.

사용자가 Docker 클라이언트로 의존성 프록시에 로그인을 시도할 때, 어디에서 JWT를 얻을지 알려주어야 합니다. 여기서 컨테이너 레지스트리와 동일한 엔드포인트를 사용할 수 있습니다: https://gitlab.com/jwt/auth. 하지만 여기서는 파라미터로 service=dependency_proxy를 지정하여 토큰을 생성하는 별도의 기본 서비스를 사용하도록 Docker 클라이언트에게 지시합니다.

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

sequenceDiagram autonumber participant C as Docker CLI participant R as GitLab (의존성 프록시) Note right of C: 사용자가 `docker login gitlab.com`을 시도하고 사용자명/비밀번호 입력 C->>R: GET /v2/ Note left of R: 인증 헤더를 확인하여, 없으면 401 반환, 토큰이 있고 유효하면 200 반환 R->>C: "WWW-Authenticate" 헤더로 401 Unauthorized 반환: "Bearer realm=\"http://gitlab.com/jwt/auth\",service=\"registry.docker.io\"" Note right of C: HTTP 기본 인증을 사용하여 Oauth 토큰 요청 C->>R: GET /jwt/auth Note left of R: 토큰이 반환됨 R->>C: Bearer 토큰이 포함된 200 OK 반환 Note right of C: 처음 요청 다시 테스트 C->>R: GET /v2/ (이번에는 `Authorization: Bearer [토큰]` 헤더와 함께) Note right of C: 로그인 성공 R->>C: 200 OK 반환

의존성 프록시는 UI(애플리케이션컨트롤러)와 API(ApiGuard)에 의해 관리되는 인증과 별도의 인증 서비스를 사용합니다. 한 번 서비스가 JWT를 생성하면, DependencyProxy::ApplicationController가 나머지 요청에 대한 인증 및 권한을 관리합니다. 이는 GitHttpClientController에서 Git 클라이언트 요청에서 구현된 인증과 유사합니다.

캐싱

블롭은 주변 로직이 없는 캐시된 artifact입니다. 우리는 이들을 해시 값으로 캐시합니다. 새로운 블롭에 대한 요청을 받으면, 요청된 해시 값을 가진 블롭이 있는지 확인하고 반환합니다. 그렇지 않으면 외부 레지스트리에서 가져와 캐시에 저장합니다.

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

이러한 지식을 기반으로 매니페스트 요청을 관리하는 다음 로직을 구축했습니다:

graph TD A[매니페스트 요청 수신] --> | 우리는 캐시에 매니페스트가 있다. | B{Docker 매니페스트 HEAD 요청} A --> | 우리는 캐시에 매니페스트가 없다. | C{Docker 매니페스트 GET 요청} B --> | DB에 있는 해시 값과 일치 | D[캐시에서 매니페스트 가져오기] B --> | HEAD 요청 오류, 네트워크 장애, DockerHub에 연결할 수 없음 | D[캐시에서 매니페스트 가져오기] B --> | DB에 있는 해시 값과 불일치 | C C --> E[매니페스트를 캐시에 저장, DB에 해시 값 저장] D --> F E --> F[매니페스트 반환]

파일 처리를 위한 Workhorse

파일 업로드 및 캐싱 관리는 Workhorse에서 이루어집니다. 이는 우리가 종속성 프록시를 위해 가진 추가적인 POST 라우트를 설명합니다.

send_dependency 메서드는 외부 레지스트리에서 이전에 가져온 JWT를 포함하여 Workhorse에 요청을 보냅니다. 그런 다음 Workhorse는 해당 토큰을 사용하여 사용자가 처음에 요청한 매니페스트 또는 블롭을 요청할 수 있습니다. 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: DB 확인. 매니페스트가 캐시에 유지되었습니까? alt 캐시에 존재 Rails->>Workhorse: send-url 인젝터로 응답 Workhorse->>Client: 파일을 클라이언트에게 전송 else 캐시에 없음 Rails->>Rails: 상위 레지스트리의 매니페스트에 대한 인증 토큰 및 다운로드 URL 생성 Rails->>Workhorse: send-dependency 인젝터로 응답 Workhorse->>외부 레지스트리: 매니페스트 요청 외부 레지스트리->>Workhorse: 매니페스트 다운로드 Workhorse->>Rails: GET /v2/*group_id/dependency_proxy/containers/*image/manifest/*tag/authorize Rails->>Workhorse: 업로드 지시로 응답 Workhorse->>Client: 원본 헤더를 포함하여 매니페스트 파일을 클라이언트에게 전송 Workhorse->>Object Storage: 매니페스트 파일과 해당 헤더 값 중 일부를 저장 Workhorse->>Rails: 업로드 완료 end

정리 정책

종속성 프록시의 정리 정책은 시간에 따른 정책으로 작동합니다. 사용자들은 파일이 읽힌 후 유지되는 날 수를 설정할 수 있습니다. 이미지에 속한 블롭을 관련 짓는 방법이 없기 때문에(이를 위해서는 컨터이너 레지스트리 팀이 구축한 메타데이터 데이터베이스를 구축해야 합니다), “이 블롭이 90일 동안 가져와지지 않았다면 삭제”와 같은 규칙을 설정할 수 있습니다. 이는 계속해서 가져와지는 파일은 캐시에서 제거되지 않지만, 예를 들어 alpine:latest가 변경되고 그 하위 블롭 중 하나가 더 이상 사용되지 않는다면, 가져와지지 않게 될 것입니다. 우리는 read_at 속성을 사용하여 주어진 dependency_proxy_blob 또는 dependency_proxy_manifest가 가져온 마지막 시간을 추적합니다.

이는 DependencyProxy::CleanupDependencyProxyWorker라는 cron 워커를 사용하여 작동하며 블롭 및 매니페스트를 삭제할 한정 용량 워커를 호출합니다. 이용 가능한 용량은 application setting에서 설정됩니다.

역사적인 참조 링크