静的オブジェクトの外部ストレージ

GitLab 12.3 で導入されました

リポジトリの静的オブジェクト(アーカイブや生のblobなど)をコンテンツデリバリーネットワークのような内部ストレージから提供するようにGitLabを設定します(CDN)。

外部ストレージの設定

静的オブジェクトの外部ストレージを設定します:

  1. 左のサイドバーで、Search を選択するか、次のページに進んでください。
  2. Admin Areaを選択します。
  3. 左側のサイドバーで、設定 > リポジトリ を選択します。
  4. リポジトリ静的オブジェクトの外部ストレージ]セクションを展開します。
  5. ベース URL と任意のトークンを入力します。外部ストレージを設定するときは、これらの値をORIGIN_HOSTNAMESTORAGE_TOKENとして設定するスクリプトを使用します。
  6. 変更を保存を選択します。

ユーザーが外部ストレージを回避してアプリケーションに直接アクセスしないように、トークンは外部ストレージからのリクエストを区別するために必要です。GitLab はこのトークンが外部ストレージからのリクエストのX-Gitlab-External-Storage-Token ヘッダに設定されることを期待しています。

非公開静的オブジェクトの提供

GitLab は非公開プロジェクトに属する静的オブジェクトの URL に対してユーザー固有のトークンを付加します。

外部ストレージからのリクエストを処理するとき、GitLabはユーザーがリクエストされたオブジェクトにアクセスできることを確認するために以下をチェックします:

  • token クエリパラメータ。
  • X-Gitlab-Static-Object-Token ヘッダ。

リクエストの流れの例

以下の例は、リクエストとレスポンスの一連の流れを示しています:

  • ユーザー
  • GitLab.
  • コンテンツ・デリバリー・ネットワーク。
sequenceDiagram 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 object not in cache 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 object in cache 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(Function as a Service)システムも同じ原理で動作するはずです。

  1. Cloudflare Workerのドメインを選択してください。
  2. 以下のスクリプトで、最初の2つの定数に以下の値を設定します:

    • ORIGIN_HOSTNAMEGitLab インストールのホスト名。
    • 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 の値をコピーしてください。これらの値を使用して、静的オブジェクトの外部ストレージを設定します。