정적 객체를 위한 외부 스토리지

Tier: Free, Premium, Ultimate Offering: Self-managed

GitLab을 구성하여 리포지토리의 정적 객체(아카이브 또는 원시 블롭과 같은)를 콘텐츠 배달 네트워크(CDN)와 같은 외부 스토리지에서 제공합니다.

외부 스토리지 구성하기

정적 객체를 위한 외부 스토리를 구성하려면:

  1. 왼쪽 사이드바에서 아래쪽으로 Admin을 선택합니다.
  2. Settings > Repository를 선택합니다.
  3. External storage for repository static objects를 확장합니다.
  4. 기본 URL과 임의의 토큰을 입력합니다. 외부 스토리지 설정을 할 때, 이 값을 ORIGIN_HOSTNAMESTORAGE_TOKEN으로 설정하는 스크립트를 사용합니다.
  5. Save changes를 선택합니다.

토큰은 외부 스토리지에서 오는 요청을 구분하는 데 필요하므로 사용자가 외부 스토리지를 우회하여 애플리케이션에 직접 접근하는 것을 방지합니다. 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: Location: 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 또는 Function as a Service (FaaS) 시스템도 동일한 원칙을 사용하여 작동해야 합니다.

  1. 아직 선택하지 않았다면 Cloudflare Worker 도메인을 선택합니다.
  2. 다음 스크립트에서 처음 두 개의 상수에 대해 다음 값을 설정합니다:

    • ORIGIN_HOSTNAME: GitLab 설치의 호스트 네임.
    • STORAGE_TOKEN: 임의의 안전한 토큰. UNIX 머신에서 pwgen -cn1 64를 실행하여 토큰을 얻을 수 있습니다. 이 토큰을 구성하는 섹션에 설명된 대로 Admin 영역에 저장하세요.

      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);  
      
          // 캐시에서 반환된 응답은 변경 불가능하므로, CORS 헤더를 설정하기 위해 다시 생성합니다.  
          response = new Response(response.body, response)  
          response.headers.set('Access-Control-Allow-Origin', '*')  
      
          return response  
        } catch (e) {  
          return new Response('An error 발생했습니다!', {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) {  
        // 필수 헤더가 유효한지 확인  
        // 이것이 유효한 사전 비행 요청인지 확인합니다.  
        if (  
          request.headers.get('Origin') !== null &&  
          request.headers.get('Access-Control-Request-Method') !== null &&  
          request.headers.get('Access-Control-Request-Headers') !== null  
        ) {  
          // CORS 사전 비행 요청 처리  
          return new Response(null, {  
            headers: CORS_HEADERS,  
          })  
        } else {  
          // 표준 OPTIONS 요청 처리  
          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  
        }  
      
        // 원래 요청에 설정된 If-None-Match를 덮어쓰고 싶지 않습니다.  
        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는 Set-Cookie 헤더가 있는 응답을 캐시하지 않습니다.  
          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의 값을 복사합니다.
    이 값을 정적 객체를 위한 외부 스토리지 구성하는 데 사용하세요.