의존성 프록시

의존성 프록시는 DockerHub의 공개 레지스트리 이미지를 위한 Pull-Through-Cache입니다. 본 문서는 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를 사용하여 요청합니다.

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

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

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

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

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

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

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

인증 및 권한 부여

Docker 클라이언트가 레지스트리에 대해 인증하는 경우, 레지스트리는 클라이언트에게 JSON 웹 토큰(JWT)을 얻을 위치와 이를 그 이후 모든 요청에 사용하라고 지시합니다. 이를 통해 인증 서비스가 레지스트리와 별도의 응용프로그램에 존재할 수 있습니다. 예를 들어, GitLab 컨테이너 레지스트리는 Docker 클라이언트에게 https://gitlab.com/jwt/auth에서 토큰을 얻도록 지시합니다. 이 엔드포인트는 gitlab-org/gitlab 프로젝트의 일부이며, Rails 프로젝트 또는 웹 서비스로도 알려져 있습니다.

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

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

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 반환: "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: 200 OK (포함된 Bearer 토큰) Note right of C: 원래의 요청을 다시 테스트함 C->>R: GET /v2/ (이번에는 `Authorization: Bearer [token]` 헤더로) Note right of C: 로그인 완료 R->>C: 200 OK

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

캐싱

블롭은 주변에 논리가 없는 캐시된 아티팩트입니다. 우리는 그들을 다이제스트로 캐시합니다. 새로운 블롭 요청을 받으면 요청된 다이제스트로 캐시된 블롭이 있는지 확인하고 반환합니다. 그렇지 않으면 외부 레지스트리에서 불러와 캐시합니다.

매니페스트는 DockerHub의 요율 제한 때문에 더 복잡합니다. 매니페스트는 기본적으로 이미지를 만들기 위한 조리법입니다. 특정 이미지를 만들기 위해 필요한 블롭 디렉터리이 있습니다. 따라서, alpine:latest에는 alpine:latest 이미지를 만들 필요가 있는 블롭이 포함된 매니페스트가 있습니다. 흥미로운 점은 alpine:latest가 언제든지 변경될 수 있다는 것입니다. 따라서 우리는 매니페스트를 캐시하고 영원히 사용할 수 있다고 가정해서는 안 됩니다. 대신, 우리는 매니페스트의 다이제스트, 즉 ETag을 확인해야 합니다. 이것은 매니페스트 요청에 있어서 다이제스트를 포함하지 않는 경우가 자주 있기 때문에 흥미롭습니다. 따라서 우리는 캐시된 매니페스트가 여전히 가장 최신의 alpine:latest인지 어떻게 알 수 있을까요? DockerHub는 요율 제한에 영향을 미치지 않는 무료 HEAD 요청을 허용합니다. 이 HEAD 요청은 매니페스트 다이제스트를 반환하므로 우리는 캐시된 매니페스트가 여전히 가장 최신의 alpine:latest인지 여부를 판단할 수 있습니다.

이러한 지식을 바탕으로 우리는 매니페스트 요청을 관리하기 위한 다음과 같은 로직을 구축했습니다:

graph TD A[매니페스트 요청 수신] --> | 캐시에 매니페스트가 있는 경우.| B{Docker 매니페스트 HEAD 요청} A --> | 캐시에 매니페스트가 없는 경우.| C{Docker 매니페스트 GET 요청} B --> | 다이제스트가 DB에 있는 것과 일치 | D[캐시에서 매니페스트 가져오기] B --> | HEAD 요청 오류, 네트워크 장애, DockerHub에 연결할 수 없음 | D[캐시에서 매니페스트 가져오기] B --> | 다이제스트가 DB에 있는 것과 일치하지 않음 | C C --> E[매니페스트를 캐시에 저장, 다이제스트를 데이터베이스에 저장] 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: 전송 URL 삽입기로 응답 Workhorse->>Client: 클라이언트에 파일 전송 else 캐시되지 않음 Rails->>Rails: 상위 레지스트리에서 매니페스트를 위해 인증 토큰 및 다운로드 URL 생성 Rails->>Workhorse: 의존성 전송 삽입기로 응답 Workhorse->>External Registry: 매니페스트 요청 External Registry->>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 작업자를 사용하여 수행되며, 이는 두 가지 제한된 용량 작업자를 시작합니다. 덩어리를 삭제하는 작업자와 Manifest를 삭제하는 작업자입니다. 이 용량은 application setting에서 설정됩니다.

과거 참조 링크