はじめに
GPU を搭載した物理サーバー3台で AI推論クラスタ を構築する機会がありました。
要件は以下の通りです。
- 複数のAI推論サービスをコンテナで動かしたい
- GPU リソースを効率的に使いたい
- ノード間でモデルファイルや設定を共有したい
- ロードバランサーで冗長化し、障害時にも自動復旧したい
- WAF でアプリケーション層のセキュリティを確保したい
運用コストの点からKubernetesではなく、Docker Swarm をオーケストレーションに採用し、GlusterFS でストレージ共有、Traefik でリバースプロキシ、ModSecurity で WAF を構成しました。
本記事では、構築の全体像と各ステップのポイントを紹介します。
構成概要
アーキテクチャ図
ノード構成
| ノード | 役割 | GPU | OS | メモリ |
|---|---|---|---|---|
| node1 | フロント + マネージャ | なし | Ubuntu 24.04 | 250GB |
| node2 | AI推論 + ワーカー | あり | Ubuntu 24.04 | 1.0TB |
| node3 | AI推論 + ワーカー | あり | Ubuntu 24.04 | 1.0TB |
主要コンポーネント
| コンポーネント | 役割 |
|---|---|
| Docker Swarm | コンテナオーケストレーション・自己修復 |
| NVIDIA Container Toolkit | GPU 対応コンテナ実行環境 |
| GlusterFS | ノード間共有ストレージ (replica 3) |
| Traefik v3 | ロードバランサー + HTTPS 終端 |
| Nginx + ModSecurity + OWASP CRS | WAF (アプリケーション層防御) |
| Let's Encrypt | SSL 証明書の自動取得・更新 |
STEP 1: Docker のインストール (全ノード)
# 公式スクリプトでインストール
curl -fsSL https://get.docker.com | sudo sh
# 自動起動設定
sudo systemctl enable docker
sudo systemctl start docker
# 現在のユーザーを docker グループに追加
sudo usermod -aG docker $USER
newgrp docker
# 確認
docker version
STEP 2: Docker Swarm クラスタ構成
クラスタ初期化 (node1)
# node1 で実行
docker swarm init --advertise-addr <node1のIP>
出力されるトークンを控えておきます。
ワーカーノードの参加 (node2, node3)
# node2, node3 で実行
docker swarm join --token <取得したトークン> <node1のIP>:2377
ノードラベルの付与
ラベルにより、サービスの配置先を制御します。
# node1 で実行
docker node update --label-add role=frontend node1
docker node update --label-add role=frontend node2
docker node update --label-add role=gpu node2
docker node update --label-add role=gpu node3
-
role=frontend: Traefik (ロードバランサー) を配置するノード -
role=gpu: AI推論コンテナを配置するノード
ポイント: hostname の事前設定
Swarm に参加後はノード名が変更できません。docker swarm join の前に hostname を設定しておきます。
# 各ノードで実行
sudo hostnamectl set-hostname node1 # node2, node3 も同様
確認
docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
xxxxx * node1 Ready Active Leader
yyyyy node2 Ready Active
zzzzz node3 Ready Active
STEP 3: GPU ドライバ + NVIDIA Container Toolkit (node2, node3)
NVIDIA ドライバのインストール
sudo apt update && sudo apt install -y nvidia-driver-550
sudo reboot
再起動後、nvidia-smi でドライバが認識されていることを確認します。
NVIDIA Container Toolkit のインストール
# GPG キーの追加
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
# リポジトリ追加
distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
sudo tee /etc/apt/sources.list.d/nvidia-docker.list
# インストール
sudo apt update && sudo apt install -y nvidia-docker2
sudo systemctl restart docker
Docker の GPU デフォルトランタイム設定
AI 推論ノードでは、Docker のデフォルトランタイムを NVIDIA に設定しておくと便利です。
/etc/docker/daemon.json:
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}
sudo systemctl restart docker
動作確認
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
nvidia-smi の出力が表示されれば成功です。
STEP 4: GlusterFS (共有ストレージ)
3ノードで replica 3 のボリュームを構成し、モデルファイル・設定・ログを共有します。
インストール (全ノード)
sudo apt update
sudo apt install -y glusterfs-server
sudo systemctl enable --now glusterd
ピア接続 (node1 から実行)
gluster peer probe node2
gluster peer probe node3
gluster peer status
事前に
/etc/hostsまたは DNS でノード名が解決できるよう設定しておく必要があります。
ボリューム作成 (node1)
sudo mkdir -p /gluster/brick1
sudo gluster volume create gv0 replica 3 transport tcp \
node1:/gluster/brick1 \
node2:/gluster/brick1 \
node3:/gluster/brick1 force
sudo gluster volume start gv0
マウント (全ノード)
sudo mkdir -p /mnt/gv0
sudo mount -t glusterfs node1:/gv0 /mnt/gv0
/etc/fstab に追記して永続化:
node1:/gv0 /mnt/gv0 glusterfs defaults,_netdev 0 0
_netdevを忘れると、ネットワーク起動前にマウントしようとして起動が失敗します。
ディレクトリ構成例
GlusterFS 上に以下のようなディレクトリ構成を作り、全ノードで共有します。
/mnt/gv0/
├─ service/
│ ├─ node1/
│ │ ├─ traefik/ # Traefik 設定ファイル
│ │ ├─ log/traefik/ # Traefik ログ
│ │ └─ ssl/ # SSL 証明書関連
│ ├─ node2/
│ └─ node3/
├─ app/
│ ├─ web/ # Web サービス
│ └─ ai/ # AI アプリ・モデルファイル
└─ docker-images/ # Docker イメージの共有用
STEP 5: Nginx + WAF + Traefik (リバースプロキシ構成)
本構成では、Nginx (ModSecurity) と Traefik を同一ノード上に配置し、以下の流れでリクエストを処理します。
Client → Nginx (WAF + SSL終端) → Traefik (ルーティング) → APP コンテナ
Traefik 単体には WAF 機能がないため、Nginx + ModSecurity を前段に置いてアプリケーション層の防御を行います。
5-1. Nginx + ModSecurity v3 のセットアップ
ModSecurity v3 のビルド (Ubuntu 24.04)
# 依存パッケージ
sudo apt install -y git build-essential libpcre3 libpcre3-dev \
libxml2 libxml2-dev libyajl-dev curl pkg-config libtool \
automake autoconf zlib1g-dev libcurl4-openssl-dev libgeoip-dev \
liblmdb-dev libpcre++-dev doxygen nginx libssl-dev
# ModSecurity v3 ビルド
cd /usr/local/src
sudo git clone --depth 1 -b v3/master https://github.com/SpiderLabs/ModSecurity
cd ModSecurity
sudo git submodule init && sudo git submodule update
sudo ./build.sh && sudo ./configure && sudo make && sudo make install
# Nginx 用コネクタ
cd /usr/local/src
sudo git clone https://github.com/SpiderLabs/ModSecurity-nginx.git
# Nginx 動的モジュールとしてビルド
NGINX_VERSION=$(nginx -v 2>&1 | grep -o '[0-9.]*')
sudo apt source nginx
cd nginx-$NGINX_VERSION
sudo ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
sudo make modules
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules
ModSecurity 設定
sudo mkdir -p /etc/nginx/modsec
cd /etc/nginx/modsec
sudo wget https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended \
-O modsecurity.conf
# 検知モードから遮断モードへ変更
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' modsecurity.conf
OWASP CRS を導入して、SQLi・XSS 等のアプリケーション層攻撃を検知・遮断します。
Nginx 設定
/etc/nginx/nginx.conf の先頭に追加:
load_module modules/ngx_http_modsecurity_module.so;
バーチャルホスト設定例 (/etc/nginx/sites-available/your-domain):
server {
listen 443 ssl;
server_name your-domain.example.com;
ssl_certificate /etc/nginx/ssl/your-domain.crt;
ssl_certificate_key /etc/nginx/ssl/your-domain.key;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/modsecurity.conf;
location / {
proxy_pass http://localhost:8080; # Traefik へ転送
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
}
sudo ln -s /etc/nginx/sites-available/your-domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
ログローテーション
/etc/logrotate.d/modsecurity:
/var/log/modsecurity/audit.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 root adm
sharedscripts
postrotate
systemctl reload nginx > /dev/null 2>&1 || true
endscript
}
5-2. Traefik のセットアップ
Overlay ネットワークの作成
docker network create --driver=overlay traefik-net
docker network create --driver=overlay app-net
Traefik 静的設定 (traefik.yaml)
entryPoints:
web:
address: ":8080"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
file:
filename: "/traefik-dynamic.yaml"
watch: true
log:
level: INFO
filePath: "/var/log/traefik/traefik.log"
accessLog:
filePath: "/var/log/traefik/access.log"
bufferingSize: 100
Nginx が SSL 終端とポート 443 を担当するため、Traefik は 8080 番で HTTP のみ受け付けます。
Traefik 動的設定 (traefik-dynamic.yaml)
http:
routers:
frontend:
rule: "Host(`your-domain.example.com`)"
service: frontend-service
entryPoints:
- web
services:
frontend-service:
loadBalancer:
servers:
- url: "http://<APPコンテナのIP>:<ポート>"
Swarm Stack 定義 (compose.yaml)
version: "3.8"
services:
traefik:
image: traefik:v3
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "/mnt/gv0/service/node1/traefik/traefik.yaml:/traefik.yaml:ro"
- "/mnt/gv0/service/node1/traefik/traefik-dynamic.yaml:/traefik-dynamic.yaml:ro"
- "/mnt/gv0/service/node1/log/traefik:/var/log/traefik"
networks:
- traefik-net
- app-net
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
deploy:
replicas: 1
placement:
constraints:
- node.labels.role == frontend
networks:
traefik-net:
external: true
app-net:
external: true
docker stack deploy -c compose.yaml traefik
STEP 6: AI/APP コンテナのデプロイ
Docker イメージの配布 (GlusterFS 経由)
プライベートレジストリを立てなくても、GlusterFS 共有ディレクトリを中継してイメージを配布できます。
# node1: イメージを tar で保存
sudo docker save -o /mnt/gv0/docker-images/ai-app.tar ai-app:latest
# node2, node3: イメージをロード
sudo docker load -i /mnt/gv0/docker-images/ai-app.tar
docker saveはイメージサイズによって10分程度かかることがあります。
GPU 対応サービスの Stack 定義
services:
ai-service:
image: ai-app:latest
deploy:
replicas: 2
placement:
constraints:
- node.labels.role == gpu
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- type: bind
source: /mnt/gv0/app/ai
target: /app/models
networks:
- app-net
docker stack deploy -c ai-stack.yaml ai
-
node.labels.role == gpuの制約で GPU ノードにのみ配置 -
resources.reservations.devicesで GPU リソースを明示的に予約 - GlusterFS マウントによりモデルファイルを全ノードで共有
運用で学んだこと・ハマりポイント
1. Swarm 参加前に hostname を設定する
Swarm に参加後はノード名が変更できません。docker swarm join の前に hostnamectl set-hostname を実行しておかないと、後から管理が煩雑になります。
2. Docker イメージの配布に GlusterFS が使える
プライベートレジストリを立てなくても、docker save → GlusterFS 共有 → docker load のワークフローで十分に運用できました。ただし大きなイメージだと保存に時間がかかるため、並行作業の計画が必要です。
3. ModSecurity のソースビルドは依存関係が多い
Ubuntu 24.04 でのビルドは依存パッケージが多く、手動構築は時間がかかります。再現性を確保するためにスクリプト化しておくのがおすすめです。
4. GlusterFS の fstab には _netdev が必須
_netdev を忘れると、ネットワーク起動前にマウントしようとして起動が失敗します。
5. daemon.json の設定を忘れると GPU コンテナが動かない
GPU ノードでは /etc/docker/daemon.json に NVIDIA ランタイムを設定した上で Docker を再起動する必要があります。これを忘れると docker load したイメージが GPU を認識しません。
まとめ
| 項目 | 採用技術 |
|---|---|
| オーケストレーション | Docker Swarm |
| GPU コンテナ | NVIDIA Container Toolkit |
| 共有ストレージ | GlusterFS (replica 3) |
| ロードバランサー | Traefik v3 |
| WAF + リバースプロキシ | Nginx + ModSecurity v3 + OWASP CRS |
| SSL | Let's Encrypt |
Docker Swarm は Kubernetes に比べて学習コストが低く、3ノード程度の小〜中規模クラスタには十分な機能を提供します。GlusterFS との組み合わせにより、プライベートレジストリなしでもイメージ配布やモデル共有が実現でき、実用的な AI 推論基盤を構築できました。
同様の構成を検討されている方の参考になれば幸いです。