외부 저장소에 대한 정적 객체

티어: Free, Premium, Ultimate 제공: 자체 관리

GitLab을 구성하여 아카이브 또는 원시 블롭과 같은 저장소 정적 객체를 외부 저장소(콘텐츠 전송 네트워크(CDN)와 같은)에서 제공하도록 구성합니다.

외부 저장소 구성

정적 객체용 외부 저장소를 구성하려면 다음을 수행합니다:

  1. 왼쪽 사이드바에서 맨 아래에서 관리자를 선택합니다.
  2. 설정 > 저장소를 선택합니다.
  3. 저장소 정적 객체를 위한 외부 저장소를 확장합니다.
  4. 기본 URL과 임의의 토큰을 입력합니다. 외부 저장소 설정에서 이들 값을 ORIGIN_HOSTNAMESTORAGE_TOKEN으로 설정하는 스크립트를 사용합니다.
  5. 변경 사항 저장을 선택합니다.

사용자가 외부 저장소를 우회하여 응용 프로그램에 직접 액세스하지 못하게 하기 위해 토큰이 필요하며, GitLab은 외부 저장소에서 생성된 요청의 X-Gitlab-External-Storage-Token 헤더에 이 토큰이 설정되기를 예상합니다.

개인용 정적 객체 제공

GitLab은 개인 프로젝트에 속하는 정적 객체 URL에 대해 사용자별 토큰을 추가하여 외부 저장소가 사용자를 대신하여 인증할 수 있도록 합니다.

외부 저장소에서 생성된 요청 처리 시, GitLab은 사용자가 요청된 객체에 액세스할 수 있는지 확인하기 위해 다음을 확인합니다:

  • token 쿼리 매개변수.
  • X-Gitlab-Static-Object-Token 헤더.

요청 흐름 예시

다음 예시에서는 사용자, GitLab, 콘텐츠 전송 네트워크 간에 발생하는 일련의 요청과 응답을 보여줍니다.

%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram accTitle: 요청 및 응답 흐름 accDescr: 사용자, GitLab, CDN 간의 요청 및 응답 흐름을 설명합니다. User->>GitLab: GET /project/-/archive/master.zip GitLab->>User: 302 Found Note over User,GitLab: 위치: https://cdn.com/project/-/archive/master.zip?token=secure-user-token User->>CDN: GET /project/-/archive/master.zip?token=secure-user-token alt 캐시에 객체 없음 CDN->>GitLab: GET /project/-/archive/master.zip Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token GitLab->>CDN: 200 OK CDN->>User: master.zip else 캐시에 객체 있음 CDN->>GitLab: GET /project/-/archive/master.zip Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token<br/>If-None-Match: etag-value GitLab->>CDN: 304 Not Modified CDN->>User: master.zip end

외부 저장소 설정

본 절차는 외부 저장소로 Cloudflare Workers을 사용하지만, 다른 CDN이나 서비스 기능(FaaS) 시스템을 동일한 원칙을 적용하여 작동할 수 있습니다.

  1. 아직 하지 않은 경우, Cloudflare Worker 도메인을 선택합니다.
  2. 다음 스크립트에서 첫 두 상수에 대한 다음 값을 설정합니다:

    • ORIGIN_HOSTNAME: GitLab 설치의 호스트명.
    • STORAGE_TOKEN: 임의의 안전한 토큰. UNIX 기기에서 pwgen -cn1 64 명령을 실행하여 토큰을 가져올 수 있습니다. 이 토큰은 정적 객체용 외부 저장소를 구성하는 섹션에 설명된 대로 관리자 영역에 저장합니다.

      const ORIGIN_HOSTNAME = 'gitlab.installation.com' // FIXME: SET CORRECT VALUE
      const STORAGE_TOKEN = 'very-secure-token' // FIXME: SET CORRECT VALUE
      const CACHE_PRIVATE_OBJECTS = false
      
      const CORS_HEADERS = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
        'Access-Control-Allow-Headers': 'X-Csrf-Token, X-Requested-With',
      }
      
      self.addEventListener('fetch', event => event.respondWith(handle(event)))
      
      async function handle(event) {
        try {
          let response = await verifyAndHandle(event);
      
          // responses returned from cache are immutable, so we recreate them
          // to set CORS headers
          response = new Response(response.body, response)
          response.headers.set('Access-Control-Allow-Origin', '*')
      
          return response
        } catch (e) {
          return new Response('An error occurred!', {status: e.statusCode || 500})
        }
      }
      
      async function verifyAndHandle(event) {
        if (!validRequest(event.request)) {
          return new Response(null, {status: 400})
        }
      
        if (event.request.method === 'OPTIONS') {
          return handleOptions(event.request)
        }
      
        return handleRequest(event)
      }
      
      function handleOptions(request) {
        // Make sure the necessary headers are present
        // for this to be a valid pre-flight request
        if (
          request.headers.get('Origin') !== null &&
          request.headers.get('Access-Control-Request-Method') !== null &&
          request.headers.get('Access-Control-Request-Headers') !== null
        ) {
          // Handle CORS pre-flight request
          return new Response(null, {
            headers: CORS_HEADERS,
          })
        } else {
          // Handle standard OPTIONS request
          return new Response(null, {
            headers: {
              Allow: 'GET, HEAD, OPTIONS',
            },
          })
        }
      }
      
      async function handleRequest(event) {
        let cache = caches.default
        let url = new URL(event.request.url)
        let static_object_token = url.searchParams.get('token')
        let headers = new Headers(event.request.headers)
      
        url.host = ORIGIN_HOSTNAME
        url = normalizeQuery(url)
      
        headers.set('X-Gitlab-External-Storage-Token', STORAGE_TOKEN)
        if (static_object_token !== null) {
          headers.set('X-Gitlab-Static-Object-Token', static_object_token)
        }
      
        let request = new Request(url, { headers: headers })
        let cached_response = await cache.match(request)
        let is_conditional_header_set = headers.has('If-None-Match')
      
        if (cached_response) {
          return cached_response
        }
      
        // We don't want to override If-None-Match that is set on the original request
        if (cached_response && !is_conditional_header_set) {
          headers.set('If-None-Match', cached_response.headers.get('ETag'))
        }
      
        let response = await fetch(request, {
          headers: headers,
          redirect: 'manual'
        })
      
        if (response.status == 304) {
          if (is_conditional_header_set) {
            return response
          } else {
            return cached_response
          }
        } else if (response.ok) {
          response = new Response(response.body, response)
      
          // cache.put will never cache any response with a Set-Cookie header
          response.headers.delete('Set-Cookie')
               
          if (CACHE_PRIVATE_OBJECTS) {
            response.headers.delete('Cache-Control')
          }
      
          event.waitUntil(cache.put(request, response.clone()))
        }
      
        return response
      }
      
      function normalizeQuery(url) {
        let searchParams = url.searchParams
        url = new URL(url.toString().split('?')[0])
      
        if (url.pathname.includes('/raw/')) {
          let inline = searchParams.get('inline')
      
          if (inline == 'false' || inline == 'true') {
            url.searchParams.set('inline', inline)
          }
        } else if (url.pathname.includes('/-/archive/')) {
          let append_sha = searchParams.get('append_sha')
          let path = searchParams.get('path')
      
          if (append_sha == 'false' || append_sha == 'true') {
            url.searchParams.set('append_sha', append_sha)
          }
          if (path) {
            url.searchParams.set('path', path)
          }
        }
      
        return url
      }
      
      function validRequest(request) {
        let url = new URL(request.url)
        let path = url.pathname
      
        if (/^(.+)(\/raw\/|\/-\/archive\/)/.test(path)) {
          return true
        }
      
        return false
      }
      
  3. 이 스크립트로 새로운 워커를 만듭니다.
  4. ORIGIN_HOSTNAMESTORAGE_TOKEN의 값을 복사합니다. 이들 값은 정적 객체용 외부 저장소를 구성하는 데 사용됩니다.