AWS

rudolfs + Nginx Proxy Manager で量産可能な Git LFS サーバーを構築する

Git LFS のストレージをセルフホストしたかったので、rudolfs(S3バックエンドのLFSサーバー)と Nginx Proxy Manager(以下NPM)を組み合わせて構築しました。

セルフホストに踏み切った理由のひとつがストレージコストです。GitHub LFS はストレージとダウンロード帯域幅それぞれに課金されますが、S3 に自前でホストするとストレージ単価を大きく抑えられます。

GitHub LFS S3 Standard S3 Intelligent-Tiering
(低頻度アクセス層)
S3 Intelligent-Tiering
(Deep Archive層)
ストレージ $0.07/GB/月 $0.025/GB/月
(約1/3)
$0.0138/GB/月
(約1/5)
$0.00099/GB/月
(約1/70)
ダウンロード帯域幅 $0.0875/GB $0.09/GB $0.09/GB $0.09/GB
S3の料金は東京リージョン(ap-northeast-1)の価格です。リージョンによって異なるため、最新の正確な料金はAWS公式ページでご確認ください。

アクセス頻度の低いファイルが多い用途であれば、Intelligent-Tiering を有効にしてアーカイブ階層まで移行させることでストレージコストをさらに大幅に抑えられます。

今回の構成のポイントは以下のとおりです。

  • リポジトリの追加がUIだけで完結 — NginxやrudolfsのコンテナをいじらずNPMのUI操作だけで増やせる
  • S3バックエンドで容量を気にしなくてよい — ローカルストレージと違い、ディスク管理が不要
  • リポジトリ間のアクセス分離 — サブドメイン+Basic認証+パス制限の組み合わせで、認証済みユーザーが他リポジトリに触れない
スポンサーリンク

構成概要

flowchart TD ClientA[Git クライアント project-a 利用者] ClientB[Git クライアント project-b 利用者] subgraph Cloudflare Tunnel[Cloudflare Tunnel] end subgraph Docker NPM[Nginx Proxy Manager:80 Basic認証 / パス制限] rudolfs[rudolfs:8080 LFSサーバー] end subgraph AWS S3[(S3バケット org/project-a/... org/project-b/...)] end ClientA -- "lfs-project-a.example.com" --> Tunnel ClientB -- "lfs-project-b.example.com" --> Tunnel Tunnel --> NPM NPM -- "/api/org/project-a/\nのみ許可" --> rudolfs NPM -- "/api/org/project-b/\nのみ許可" --> rudolfs rudolfs --> S3

NPMはNginxのリバースプロキシ設定をWeb UIだけで管理できるツールです。Basic認証やSSL証明書の設定もUIから操作できるため、Nginxの設定ファイルを直接編集する手間がなく、リポジトリの追加もUIの操作だけで完結します。

S3バケットは全リポジトリで共有し、rudolfsがリポジトリごとのプレフィックスでオブジェクトを分けて保存します。S3バケット自体にアクセス制御は設けず、NPMがアクセス制御を担います。

リポジトリごとにサブドメインを割り当て、NPMのBasic認証とパス制限を組み合わせることで、認証を通過した利用者が別リポジトリのパスにアクセスできないようにしています。

前提 / 必要なもの

  • Docker / Docker Compose
  • S3バケット(AWSなど)
  • ドメイン(Cloudflare Tunnelなどで外部公開する場合)

手順

Step 1: Docker Composeの用意

以下の compose.yaml を用意します。

name: git-lfs

x-common-logging: &x-common-logging
  driver: json-file
  options:
    max-size: 10m
    max-file: 3

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    volumes:
      - npm-data:/data
      - npm-letsencrypt:/etc/letsencrypt
    networks:
      default:
      cloudflared:
        aliases:
          - lfs-npm
    logging: *x-common-logging

  lfs:
    image: jasonwhite0/rudolfs:latest
    restart: unless-stopped
    command: s3 --bucket=${S3_BUCKET}
    environment:
      RUDOLFS_KEY: "${RUDOLFS_KEY}"
      AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
      AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
      AWS_DEFAULT_REGION: "${AWS_REGION}"
    logging: *x-common-logging

volumes:
  npm-data:
  npm-letsencrypt:

networks:
  cloudflared:
    external: true

cloudflared ネットワークはCloudflare TunnelのコンテナがいるDockerネットワークです。Cloudflare Tunnelを使わない場合はネットワーク設定を適宜変更してください。

.env ファイルも作成します。

RUDOLFS_KEY=        # openssl rand -hex 32 で生成
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
S3_BUCKET=

RUDOLFS_KEYopenssl rand -hex 32 で生成した値を設定します。S3オブジェクトの暗号化キーです。

docker compose up -d

Step 2: S3バケット・IAMの設定

IAMポリシーを作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::{バケット名}",
        "arn:aws:s3:::{バケット名}/*"
      ]
    }
  ]
}

IAMユーザーを作成し、このポリシーをアタッチします。アクセスキーを発行して .env に設定してください。

コストを抑えたい場合、S3バケットにライフサイクルルールを設定しておくのがおすすめです。以下は設定例です。アクセス頻度に応じて日数は調整してください。

設定 値(例)
Intelligent-Tiering へ移行 0日(即時)
Archive Access 移行 360日
Deep Archive Access 移行 720日

Step 3: NPMでAccess Listを作成

NPMの管理UI(81番ポート)にアクセスし、リポジトリ用のAccess Listを作成します。

アクセスリスト → アクセスリストを追加

  • 名前: プロジェクト名
  • 認証タブ: ユーザー名・パスワードを追加
  • ルール: allow / all を追加

Step 4: NPMでProxy Hostを追加

プロキシホスト → プロキシホストを追加

項目
ドメイン名 lfs-{プロジェクト名}.example.com
転送ホスト名/IP lfs
転送ポート 8080
アクセスリスト 手順3で作成したもの

カスタムNginx設定(右上の歯車アイコン)に以下を設定します。{org}{repo} は実際の値に置き換えてください。

client_max_body_size 0;

location ~ ^/api/{org}/{repo}/object/ {
    auth_basic off;
    proxy_pass http://lfs:8080;
}

location ~ ^/api/{org}/{repo}/objects/verify {
    auth_basic off;
    proxy_pass http://lfs:8080;
}

location ~* ^(?!/api/{org}/{repo}/) {
    return 403;
}

なぜ auth_basic off が必要か
rudolfsはバッチAPIのレスポンスに "authenticated": true を含めます。これはgit LFSプロトコルの仕様で「URLが自己認証済み」を示すフラグで、クライアントはアップロード(PUT)・verifyリクエストに認証情報を付けなくなります。NPMのAccess Listは全リクエストにBasic認証を要求するため、そのままではPUT・verifyが401になってしまいます。
そのためアップロード・verifyのエンドポイントは auth_basic off で個別に無効化します。アップロードURLの末尾はSHA256ハッシュ(OID)なので、URLが推測されても任意のコンテンツをアップロードすることは不可能です。
Custom Locations タブの Advanced フィールドは使わない
Custom LocationsタブのAdvancedフィールドはAccess Listの auth_basic 設定が継承されるため、auth_basic 系のディレクティブを追記するとnginxの設定生成がエラーになります(エラー表示はなし)。auth_basic のカスタム設定は必ずSettingsボタン(カスタムNginx設定)から行ってください。

Step 5: リポジトリに .lfsconfig を追加

[lfs]
    url = https://{user}:{password}@lfs-{プロジェクト名}.example.com/api/{org}/{repo}
    locksverify = false

locksverify = false はrudolfsがLFSロックAPIに非対応なため必要です。

認証情報の管理について
この例はアクセス制御されたプライベートなリポジトリを想定しているため、Basic認証の情報を .lfsconfig に直接記載しています。パブリックなリポジトリや認証情報をリポジトリに含めたくない場合は、.lfsconfig にはURLのみ記載し、認証情報は ~/.netrc やgit credentialで別管理にしてください。
rudolfsのURLルーティングについて
rudolfsのLFS APIエンドポイントは /api/{org}/{project}/ プレフィックスが必須です。/objects/batch/{org}/objects/batch のように省略すると404になります。.lfsconfigurl には必ず2階層のパスを含めてください。

リポジトリの追加

2つ目以降のリポジトリを追加する場合は、Step 3〜5を繰り返すだけです。

  • Cloudflare Tunnelを使っている場合はサブドメインのルート追加も必要です
  • S3バケットは共有のまま使い回せます
  • rudolfsのコンテナは追加不要です

おわりに

NPMのAccess Listとカスタムエンドポイント制限の組み合わせで、わりとシンプルにマルチリポジトリのLFSサーバーが作れました。
Amazon S3でなくとも、互換のストレージが使用できますので、MinIO や RustFS などでサーバーのディスク領域を活用することもできますね。

なお、auth_basic off が必要な部分はハマりポイントだったので、ややモヤっとしますが、同じ構成を試す方は気を付けてください。

以上!

コメント

タイトルとURLをコピーしました