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 クラスターの話はこちら。
やったこと
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
ReadWritePaths は data_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 NODES に NO 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 connect → layout assign → layout apply
|
| 8 | バケット・アクセスキー作成 |
| 9 | AWS CLI で動作確認 |
| 10 | k3s + HAProxy でラウンドロビン負荷分散 |
| 11 | Garage Web UI のデプロイ |
Garage は単一バイナリで動作し、設定もシンプルである。「余った PC と持て余していた Raspberry Pi で耐障害性のある S3 ストレージを 3 拠点に構える」という目的に対してフィットした。
同じ構成を試みる方の参考になれば幸いである。
