分散エージェントシステムのコンテナ化における課題
多エージェント(AI Bot)協調システムは、タスク分散、コード生成、インフラ監視、テスト実行などの役割を分担し、メッセージキューとHTTP API経由で通信します。このような分散システムをDocker環境で本番稼働させる際、ローカル開発とは異なる様々な運用課題に直面します。本記事では、コンテナ化に伴う主要な技術的課題とその解決アプローチについて解説します。
サービスディスカバリと起動順序の制御
コンテナは独立したネットワーク名前空間を持つため、localhost での通信は機能しません。また、Docker Composeの depends_on はコンテナの起動のみを保証し、アプリケーションの準備完了(例:大規模モデルのロード)までは保証しません。これにより、依存先が初期化中にリクエストを受け取り、接続エラーが発生する問題が起きます。
解決策として、サービス名によるDNS解決と、ヘルスチェックの start_period を活用した起動制御を実装します。
services:
router-agent:
build:
context: ./agents/router
ports:
- "8080:8080"
networks:
- agent-mesh
depends_on:
generator-agent:
condition: service_healthy
environment:
GENERATOR_ENDPOINT: http://generator-agent:8081
BROKER_URL: redis://broker:6379
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/ping"]
interval: 15s
timeout: 5s
retries: 3
start_period: 40s
restart: unless-stopped
generator-agent:
build:
context: ./agents/generator
ports:
- "8081:8081"
networks:
- agent-mesh
environment:
MODEL_DIR: /opt/models
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8081/ping"]
interval: 15s
timeout: 10s
retries: 5
start_period: 90s
restart: unless-stopped
broker:
image: redis:7-alpine
networks:
- agent-mesh
volumes:
- broker-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
networks:
agent-mesh:
driver: bridge
volumes:
broker-data:
start_period の設定が不可欠です。これを設定しないと、重いモデルの読み込み中にヘルスチェックが失敗し、コンテナが無限に再起動するループに陥ります。
開発環境と本番環境のビルド分離
開発時のホットリロードと本番時の最適化されたバイナリを同一のDockerfileで管理するには、マルチステージビルドが有効です。これにより、環境差異によるバグを未然に防ぎます。
# ベースイメージ
FROM python:3.12-slim AS core
WORKDIR /srv
COPY pyproject.toml .
RUN pip install --no-cache-dir poetry && \
poetry config virtualenvs.create false && \
poetry install --no-dev
# 開発用ステージ
FROM core AS development
RUN poetry install
CMD ["python", "-m", "watchfiles", "run", "main.py"]
# 本番用ステージ
FROM core AS production
COPY ./app ./app
COPY ./configs ./configs
RUN python -m compileall app/
CMD ["gunicorn", "app.main:application", "--bind", "0.0.0.0:8080", "--workers", "4"]
環境変数は .env (共通), .env.local (開発), .env.production (本番) に分割し、Composeファイルの env_file で読み込むことで、同一のオーケストレーション定義を再利用できます。
分散ログの構造化と集約
複数コンテナのログを個別に追跡するのは困難です。agent_id, trace_id, timestamp を含む構造化ログ(JSON形式)を出力し、軽量なログルーターで集約します。
[sources.docker_logs]
type = "docker_logs"
auto_partial_merge = true
[transforms.parse_json]
type = "remap"
inputs = ["docker_logs"]
source = '''
. = parse_json!(.message)
'''
[sinks.elasticsearch]
type = "elasticsearch"
inputs = ["parse_json"]
endpoints = ["http://log-store:9200"]
bulk.index = "agent-logs-%Y.%m.%d"
障害発生時には trace_id をキーに検索することで、複数エージェントにまたがるリクエストの全体像を即座に特定できます。
擬似正常状態を防ぐ深度ヘルスチェック
単にプロセスが生存しているだけでは不十分です。モデルのウォームアップ状態やキューの滞留状況を考慮した深度ヘルスチェックを実装し、見かけ上の正常状態(偽陽性)を防ぎます。
from fastapi import FastAPI, Response
import time
app = FastAPI()
last_processed_at = time.time()
@app.get("/readiness")
async def readiness_probe():
metrics = {
"runtime_active": True,
"model_warm": model_engine.is_warmed_up(),
"idle_seconds": time.time() - last_processed_at,
"backlog_size": task_queue.unprocessed_count(),
}
if not metrics["model_warm"]:
return Response(status_code=503, json=metrics)
# 長時間未処理かつタスク滞留がある場合、フリーズとみなす
if metrics["idle_seconds"] > 600 and metrics["backlog_size"] > 0:
return Response(status_code=503, json=metrics)
# キューが許容量を超えている場合
if metrics["backlog_size"] > 500:
return Response(status_code=503, json=metrics)
return metrics
ローリングアップデートと自動ロールバック
単純な up -d によるデプロイは、障害発生時の復旧を遅らせます。依存関係に基づいたローリングアップデートと、ヘルスチェック失敗時の自動ロールバック機構をシェルスクリプトで実装します。
#!/usr/bin/env bash
set -euo pipefail
RELEASE_VERSION=${1:?"Usage: $0 <version-tag>"}
STATE_FILE=".stable_release"
echo "🚀 Deploying version: ${RELEASE_VERSION}"
if [[ -f "$STATE_FILE" ]]; then
STABLE_VERSION=$(cat "$STATE_FILE")
echo "📌 Current stable version: ${STABLE_VERSION}"
fi
export APP_VERSION=$RELEASE_VERSION
TARGET_SERVICES=("broker" "generator-agent" "monitor-agent" "tester-agent" "router-agent")
DEPLOYMENT_FAILED=false
for service in "${TARGET_SERVICES[@]}"; do
echo "🔄 Updating ${service}..."
docker compose up -d --no-deps "$service"
# ヘルスチェックの完了を待機 (最大150秒)
for attempt in {1..30}; do
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' "system-${service}-1" 2>/dev/null || echo "initializing")
if [[ "$HEALTH" == "healthy" ]]; then
echo "✅ ${service} is healthy"
break
fi
if [[ $attempt -eq 30 ]]; then
echo "❌ ${service} health check timeout"
DEPLOYMENT_FAILED=true
break
fi
sleep 5
done
if [[ "$DEPLOYMENT_FAILED" == true ]]; then
break
fi
done
if [[ "$DEPLOYMENT_FAILED" == true ]]; then
echo "⏪ Rolling back to ${STABLE_VERSION}..."
export APP_VERSION=$STABLE_VERSION
docker compose up -d --force-recreate
echo "❌ Deployment aborted. Rolled back to stable state."
exit 1
fi
echo "$RELEASE_VERSION" > "$STATE_FILE"
echo "🎉 Deployment successful. Version ${RELEASE_VERSION} is now stable."