0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Garageで作るS3互換おうちオブジェクトストレージ 〜3拠点レプリケーションを添えて〜

0
Last updated at Posted at 2026-04-24

Summary

  • MinIO OSS 版が事実上の終了を迎えたため、代替の S3 互換ストレージとして Garage を採用した
  • 自宅 PC(Ubuntu 24.04/amd64)・実家 Raspberry Pi 2(Alpine Linux/armv7)・クラウド VPS(Ubuntu 24.04/amd64)の 3 拠点を Tailscale で接続し、自動でレプリケーションが実施されるようにした
  • k3s 上のアプリケーションから HAProxy を介して接続し、 S3 互換 API でファイルの管理ができるようになった

やらないこと

  • Garageの監視
  • バージョンアップ・バックアップ運用の詳細
  • k3s、Tailscaleの構築手順

本記事における課題

なぜ MinIO を使わないのか

新たにオブジェクトストレージを構築するにあたり、MinIO は積極的に選択できない状況になっていた。

MinIO OSS 版は 2021 年のライセンス変更(Apache 2.0 → AGPL v3)を皮切りに段階的に終息し、2025 年 10 月には事前告知なしで Docker イメージの配布を停止、2026 年 2 月には GitHub リポジトリがアーカイブされメンテナンスが完全に終了した。大規模エンタープライズ向けへの方針転換が明確になった以上、小規模・個人用途で新規採用する理由はない。

経緯の詳細は「MinIO、知らないうちに終了していた(Qiita)」が詳しい。

Garage を選んだ理由

Garage は Rust 製の S3 互換オブジェクトストレージである。MinIO と異なり、地理的に分散した小規模クラスターに特化して設計されており、以下の点が採用理由となった。

特徴 内容
軽量・単一バイナリ 依存ライブラリなし、musl 静的リンクで配布
S3 互換 API 既存アプリをそのまま接続できる
分散レプリケーション ゾーン概念でデータの地理分散を制御
シンプルな設定 TOML 1 ファイルで完結
ライセンス AGPL v3(個人利用に問題なし)

特に Raspberry Pi 2(ARMv7、RAM 1GB)でも単一バイナリとして動作する軽量さが決め手となった。

構成概要

実家ネットワークの遠隔保守用に設置していた Raspberry Pi 2 が常時稼働しているにもかかわらず遠隔保守以外には活用できていなかった。Garage が ARMv7 向けバイナリを提供していることを確認し、このラズパイをストレージノードとして活用することにした。

3 拠点はいずれもグローバル IP が固定ではないため、Tailscale で VPN を構成している。rpc_public_addr には各ノードの Tailscale IP(100.x.x.x)を指定することで NAT 越えを解決した。

以前組んだ k3s クラスターの話はこちら

image.png

やったこと

1. ポート開放

各ノード間で以下のポートを開放する。

ポート 用途
3900/TCP S3 API エンドポイント
3901/TCP ノード間 RPC 通信
3902/TCP 静的 Web サイト配信
3903/TCP 管理 API

ファイアウォールの設定は各自の環境に合わせて実施すること

ufw・nftables・iptables・クラウドのセキュリティグループなど環境によって手順が異なるため、本記事では具体的な手順は省略する。Tailscale 経由のみでノード間通信を行う場合、Tailscale インターフェース(tailscale0)からのトラフィックを許可するルールを追加すれば十分である。

2. RPC シークレットと管理トークンの生成

クラスター内の全ノードで同じ rpc_secretを使う。どれか 1 台で生成し、控えておく。

# RPC シークレット
openssl rand -hex 32
# 例: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1

# 管理 API トークン(別途生成)
openssl rand -hex 32

以降、それぞれを <RPC_SECRET><ADMIN_TOKEN> と表記する。

3. インストール:Ubuntu 24.04(node1・node3)

Ubuntu には公式 apt リポジトリが存在しないため、静的バイナリを直接インストールする。

GARAGE_VERSION="v2.3.0"

wget "https://garagehq.deuxfleurs.fr/_releases/${GARAGE_VERSION}/x86_64-unknown-linux-musl/garage" \
  -O /tmp/garage

chmod +x /tmp/garage
/tmp/garage --version   # インストール前にバージョンが表示されることを確認

sudo mv /tmp/garage /usr/local/bin/garage
garage --version

/tmp で先に動作確認するのがおすすめ

/usr/local/bin に移動する前に /tmp/garage --version を実行しておくと、誤ったアーキテクチャのバイナリを落としていないか確認できる。

専用ユーザーとディレクトリの作成

sudo useradd --system --no-create-home --shell /bin/false garage

sudo mkdir -p /var/lib/garage/meta
sudo mkdir -p /var/lib/garage/data
sudo mkdir -p /etc/garage

sudo chown -R garage:garage /var/lib/garage
sudo chown -R garage:garage /etc/garage

metadata_dir には高速なストレージを推奨

公式ドキュメントでは、metadata_dir にはオブジェクトのインデックス走査が頻繁に発生するため、十分なスループットを持つ SSD を使うことが推奨されている。

今回は個人利用・小規模データ量のため既存ストレージをそのまま使用した。

4. インストール:Alpine Linux(node2 / Raspberry Pi 2)

アーキテクチャとバイナリ名

Raspberry Pi 2 は ARMv7(32bit)である。Garage のリリースページに armv7 という名称のバイナリは存在せず、armv6l-unknown-linux-musleabihf という名称で配布されている。musl 静的リンクのため Alpine Linux でそのまま動作する。

apk add wget   # 未導入の場合

GARAGE_VERSION="v2.3.0"

wget "https://garagehq.deuxfleurs.fr/_releases/${GARAGE_VERSION}/armv6l-unknown-linux-musleabihf/garage" \
  -O /tmp/garage

chmod +x /tmp/garage
/tmp/garage --version   # インストール前に確認

mv /tmp/garage /usr/local/bin/garage

ARMv7 向けバイナリのターゲット名に注意

リリースページで armv7 を探しても見つからない。Raspberry Pi 2 (armv7) の場合は armv6l-unknown-linux-musleabihf のバイナリをダウンロードすること。

専用ユーザーとディレクトリの作成

ユーザーを作成してもグループは自動作成されないため chown のグループ指定は不要である。

adduser -S -H -D -s /sbin/nologin garage

mkdir -p /var/lib/garage/meta
mkdir -p /var/lib/garage/data
mkdir -p /etc/garage

chown -R garage /var/lib/garage
chown -R garage /etc/garage

5. 設定ファイルの作成(全ノード共通)

各ノードで /etc/garage/garage.toml を作成する。ノードごとに異なる値は rpc_public_addr のみである。

node1(自宅 PC)の例:

metadata_dir = "/var/lib/garage/meta"
data_dir     = "/var/lib/garage/data"
db_engine    = "lmdb"

# 6時間ごとにメタデータスナップショットを取得(電源断対策)
metadata_auto_snapshot_interval = "6h"

# 全3ノードにコピーする
replication_factor = 3

compression_level = 2

rpc_bind_addr   = "[::]:3901"
rpc_public_addr = "100.xx.xx.xx:3901"   # このノードの Tailscale IP
rpc_secret      = "<RPC_SECRET>"

[s3_api]
s3_region     = "garage"
api_bind_addr = "[::]:3900"
root_domain   = ".s3.garage.local"

[s3_web]
bind_addr   = "[::]:3902"
root_domain = ".web.garage.local"
index       = "index.html"

[admin]
api_bind_addr = "[::]:3903"
admin_token   = "<ADMIN_TOKEN>"

node2・node3 も同内容で rpc_public_addr のみそれぞれの Tailscale IP に変更する。

設定後は必ず内容を確認する:

cat /etc/garage/garage.toml

6. サービスの起動

Ubuntu(node1・node3):systemd

sudo tee /etc/systemd/system/garage.service << 'EOF'
[Unit]
Description=Garage S3-compatible object storage
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=garage
Group=garage
ExecStart=/usr/local/bin/garage -c /etc/garage/garage.toml server
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/garage /etc/garage

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now garage
sudo systemctl status garage

ReadWritePathsdata_dir / metadata_dir と一致させること

ProtectSystem=strict を指定した状態で ReadWritePaths に実際のパスが含まれていないと、Permission denied でサービスが起動しない。garage.toml でパスを変更した場合は garage.service も必ず合わせて更新し daemon-reload を実行する。

ログ確認:

sudo journalctl -xeu garage

Alpine Linux(node2):OpenRC

cat > /etc/init.d/garage << 'EOF'
#!/sbin/openrc-run

name="garage"
description="Garage S3-compatible object storage"
command="/usr/local/bin/garage"
command_args="-c /etc/garage/garage.toml server"
command_user="garage"
pidfile="/run/garage.pid"
command_background=true

depend() {
    need net
    after firewall
}
EOF

chmod +x /etc/init.d/garage
rc-service garage start
rc-update add garage default
rc-service garage status

Alpine には journald がないため、ログは以下で確認する:

logread | grep garage
# またはフォアグラウンドで直接実行してエラーを確認
garage -c /etc/garage/garage.toml server

7. クラスターの結合とレイアウト設定

Step 1:各ノードの ID を確認する

全ノードで実行:

garage -c /etc/garage/garage.toml node id
# 出力例: b88f23195c7d89b1...@100.xx.xx.xx:3901

Step 2:ノードをクラスターに接続する

node1 から実行:

# node2 を接続
garage -c /etc/garage/garage.toml node connect \
  <NODE2_FULL_ID>@100.xx.xx.xx:3901

# node3 を接続
garage -c /etc/garage/garage.toml node connect \
  <NODE3_FULL_ID>@100.xx.xx.xx:3901

Step 3:接続状態の確認

garage -c /etc/garage/garage.toml status

全ノードが HEALTHY NODESNO ROLE ASSIGNED の状態で表示されれば接続成功である。

Step 4:レイアウトを設定する

各ノードにゾーン名容量を割り当てる。ゾーン名は任意の文字列でよい(拠点を識別できる名前を推奨)。同じゾーン内のノードには同じデータを重複配置しないよう Garage が自動調整する。

# node1
sudo garage -c /etc/garage/garage.toml layout assign \
  -z zone1 -c 500G <NODE1_IDの先頭8文字>

# node2
garage -c /etc/garage/garage.toml layout assign \
  -z zone2 -c 100G <NODE2_IDの先頭8文字>

# node3
garage -c /etc/garage/garage.toml layout assign \
  -z zone3 -c 50G <NODE3_IDの先頭8文字>

容量の指定はデータ分散の重みとして機能する

ここで指定する値は実ディスク容量の上限ではなく、Garage がデータを均等分散するための重みとして使われる。実ディスク容量を大幅に超えても強制停止はされないが、実容量の 70〜80% 程度を目安に指定するとバランスよく分散される。

Step 5:レイアウトの確認と適用

# プレビュー確認
garage -c /etc/garage/garage.toml layout show

# 問題なければ適用(初回は --version 1)
garage -c /etc/garage/garage.toml layout apply --version 1

# 適用後の確認
garage -c /etc/garage/garage.toml status

適用後のステータス例:

==== HEALTHY NODES ====
ID                  Hostname  Address            Zone          Capacity  DataAvail
b88f23195c7d89b1…  node1     100.xx.xx.xx:3901  zone1          500.0 GB  488.5 GB
36db2d53a337af14…  node2     100.xx.xx.xx:3901  zone2          100.0 GB   97.3 GB
ec6021b34bf6c069…  node3     100.xx.xx.xx:3901  zone3           50.0 GB   47.8 GB

8. バケットとアクセスキーの作成

# バケット作成
garage -c /etc/garage/garage.toml bucket create my-bucket

# アクセスキー作成(Key ID と Secret key が表示される。必ず控えておく)
garage -c /etc/garage/garage.toml key create my-app-key

# バケットへの権限付与
garage -c /etc/garage/garage.toml bucket allow \
  --read --write --owner \
  --key my-app-key \
  my-bucket

# 確認
garage -c /etc/garage/garage.toml bucket list
garage -c /etc/garage/garage.toml bucket info my-bucket

キーの削除は --yes が必要

key delete <name> だけでは削除されず、key delete <name> --yes とする必要がある。

9. 動作確認:AWS CLI でアップロード

# Ubuntu
sudo apt install awscli -y

# Alpine
apk add aws-cli
aws configure set aws_access_key_id     <Key ID>
aws configure set aws_secret_access_key <Secret key>
aws configure set default.region        garage
echo "Hello, Garage!" > test.txt

# アップロード
aws --endpoint-url http://100.xx.xx.xx:3900 \
  s3 cp test.txt s3://my-bucket/test.txt

# 一覧確認
aws --endpoint-url http://100.xx.xx.xx:3900 \
  s3 ls s3://my-bucket/

# 別ノード経由でも同じオブジェクトが見えることを確認(レプリケーション確認)
aws --endpoint-url http://100.xx.xx.xx:3900 \   # node2
  s3 ls s3://my-bucket/

aws --endpoint-url http://100.xx.xx.xx:3900 \   # node3
  s3 ls s3://my-bucket/

全エンドポイントから同じ結果が返れば、レプリケーション構成の完成である。

10. HAProxy によるラウンドロビン負荷分散(k3s 上)

クラスターを構築しただけでは、アプリケーション側から利用するエンドポイントを 1 つに固定すると、そのノードが落ちたときに接続できなくなる。そこで k3s 上に HAProxy をデプロイし、3 ノードへのラウンドロビン分散と自動フェイルオーバーを実現した。

構成のポイント

  • HAProxy の Pod を 3 レプリカ起動し、各 Pod を異なるノードに配置(podAntiAffinity
  • HAProxy が 3 つの Garage ノードに対してヘルスチェックを行い、障害ノードを自動切り離し(2 回連続失敗でダウン判定、2 回連続成功で復帰)
  • k3s 上のアプリからは proxy-minio-svc:3900(ClusterIP Service)に接続するだけでよい

Docker でも同様に構築可能
以下は k3s(Kubernetes)向けのマニフェストだが、haproxy.cfg の内容はそのまま流用できる。Docker 環境であれば haproxy.cfg をボリュームマウントして docker run または Docker Compose でデプロイすれば同等の構成が実現できる。

HAProxy 設定(ConfigMap)

HAProxy の動作を定義する設定ファイルを ConfigMap として管理する。<NODE*_TAILSCALE_IP> には各 Garage ノードの Tailscale IP を指定する。

apiVersion: v1
kind: ConfigMap
metadata:
  name: proxy-minio-conf
  namespace: proxy
data:
  haproxy.cfg: |
    global
      log stdout format raw local0
      maxconn 1024

    defaults
      log     global
      mode    http
      option  httplog
      option  dontlognull
      timeout connect 5s
      timeout client  30s
      timeout server  30s
      # 障害ノードの自動切り離し・復帰
      option  redispatch
      retries 3

    frontend garage_frontend
      bind *:3900
      default_backend garage_backend

    backend garage_backend
      balance roundrobin
      option  tcp-check

      # 2回連続失敗でダウン判定、2回連続成功で復帰
      default-server inter 10s fall 2 rise 2

      server garage-node1 <NODE1_TAILSCALE_IP>:3900 check
      server garage-node2 <NODE2_TAILSCALE_IP>:3900 check
      server garage-node3 <NODE3_TAILSCALE_IP>:3900 check

HAProxy Deployment と Service

apiVersion: v1
kind: Service
metadata:
  labels:
    app: proxy-minio
  name: proxy-minio-svc
  namespace: proxy
spec:
  ports:
  - port: 3900
    protocol: TCP
    targetPort: 3900
  type: ClusterIP
  selector:
    app: proxy-minio
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: proxy-minio
  name: proxy-minio
  namespace: proxy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: proxy-minio
  template:
    metadata:
      labels:
        app: proxy-minio
    spec:
      containers:
      - image: haproxy:3.3.6
        name: haproxy
        imagePullPolicy: IfNotPresent
        args:
        - -f
        - /usr/local/etc/haproxy/haproxy.cfg
        env:
        - name: TZ
          value: Asia/Tokyo
        ports:
        - containerPort: 3900
        readinessProbe:
          tcpSocket:
            port: 3900
          initialDelaySeconds: 5
          timeoutSeconds: 5
          periodSeconds: 20
          successThreshold: 1
          failureThreshold: 3
        livenessProbe:
          tcpSocket:
            port: 3900
          initialDelaySeconds: 5
          timeoutSeconds: 5
          periodSeconds: 20
          successThreshold: 1
          failureThreshold: 3
        volumeMounts:
        - mountPath: /usr/local/etc/haproxy
          name: proxy-minio-settings
      volumes:
      - name: proxy-minio-settings
        configMap:
          name: proxy-minio-conf
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - proxy-minio
            topologyKey: kubernetes.io/hostname

11. Garage Web UI のデプロイ

Garage にはブラウザで操作できる公式 Web UI が存在しないが、コミュニティ製の Garage Web UI が公開されている。バケット一覧・オブジェクトのブラウジング・アクセスキー管理などを GUI で操作できる。

Garage の管理 API(admin_token)と S3 API エンドポイントを環境変数で渡すだけで動作する。admin_token 等の機密情報は Kubernetes Secret で管理している。

Docker でも同様にデプロイ可能

以下は k3s 向けのマニフェストだが、Docker 環境であれば以下のように docker run で同等の構成が実現できる。

docker run -d \
  -e API_BASE_URL=http://<GARAGE_NODE_IP>:3903 \
  -e API_ADMIN_KEY=<ADMIN_TOKEN> \
  -e S3_ENDPOINT_URL=http://<HAPROXY_IP>:3900 \
  -v /path/to/dummy-garage.toml:/etc/garage.toml \
  -p 3909:3909 \
  khairul169/garage-webui:1.1.0

/etc/garage.toml は起動時に存在が要求されるため、空ファイルをマウントする。

Garage Web UI 設定(ConfigMap)

apiVersion: v1
kind: ConfigMap
metadata:
  name: garage-webui-conf
  namespace: proxy
data:
  garage.toml: |
    # dummy(接続情報は環境変数で渡すため空ファイルでよい)

Garage Web UI Deployment・Service・Ingress

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: garage-webui
  name: garage-webui
  namespace: proxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: garage-webui
  template:
    metadata:
      labels:
        app: garage-webui
    spec:
      containers:
      - image: khairul169/garage-webui:1.1.0
        name: garage-webui
        imagePullPolicy: IfNotPresent
        env:
        - name: TZ
          value: Asia/Tokyo
        - name: API_BASE_URL       # Garage 管理 API の URL(例: http://100.xx.xx.xx:3903)
          valueFrom:
            secretKeyRef:
              name: garage-admin-api
              key: base-url
        - name: API_ADMIN_KEY      # garage.toml の admin_token と一致させる
          valueFrom:
            secretKeyRef:
              name: garage-admin-api
              key: api-key
        - name: S3_ENDPOINT_URL
          value: http://proxy-minio-svc:3900   # HAProxy 経由で S3 API へ接続
        ports:
        - containerPort: 3909
        readinessProbe:
          tcpSocket:
            port: 3909
          initialDelaySeconds: 5
          timeoutSeconds: 5
          periodSeconds: 20
          successThreshold: 1
          failureThreshold: 3
        livenessProbe:
          tcpSocket:
            port: 3909
          initialDelaySeconds: 5
          timeoutSeconds: 5
          periodSeconds: 20
          successThreshold: 1
          failureThreshold: 3
        volumeMounts:
        - mountPath: /etc/garage.toml
          subPath: garage.toml
          name: garage-webui-settings
      volumes:
      - name: garage-webui-settings
        configMap:
          name: garage-webui-conf
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: garage-webui
  name: garage-webui-svc
  namespace: proxy
spec:
  ports:
  - port: 3909
    protocol: TCP
    targetPort: 3909
  type: ClusterIP
  selector:
    app: garage-webui
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: garage-webui-ingress
  namespace: proxy
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod          # cert-manager で TLS 自動発行
    traefik.ingress.kubernetes.io/router.middlewares: proxy-proxy-ipwhitelist@kubernetescrd  # IP 制限
spec:
  rules:
  - host: <YOUR_DOMAIN>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: garage-webui-svc
            port:
              number: 3909
  tls:
    - secretName: garage-webui-tls
      hosts:
        - <YOUR_DOMAIN>

管理 API のセキュリティ

admin_token を設定した上で、管理 API(3903 番ポート)へのアクセスは信頼できるネットワーク(Tailscale 等)からに限定することを強く推奨する。Garage Web UI を Ingress 経由で公開する場合は、IP ホワイトリストや VPN 経由アクセスの強制など、追加の保護を必ず設けること。

まとめ・所感

ステップ 実施内容
1 ポート開放(3900〜3903)
2 RPC シークレット・管理トークン生成
3・4 静的バイナリのインストール(Ubuntu: x86_64、Alpine: armv6l-musleabihf)
5 garage.toml 作成(rpc_public_addr のみノードごとに変更)
6 systemd / OpenRC でサービス起動
7 node connectlayout assignlayout apply
8 バケット・アクセスキー作成
9 AWS CLI で動作確認
10 k3s + HAProxy でラウンドロビン負荷分散
11 Garage Web UI のデプロイ

Garage は単一バイナリで動作し、設定もシンプルである。「余った PC と持て余していた Raspberry Pi で耐障害性のある S3 ストレージを 3 拠点に構える」という目的に対してフィットした。

同じ構成を試みる方の参考になれば幸いである。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?