多エージェントアーキテクチャにおけるDockerオーケストレーションの技術的課題と実践的解決策

分散エージェントシステムのコンテナ化における課題

多エージェント(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."

タグ: Docker docker-compose multi-agent-systems fastapi Vector

8月19日 12:03 投稿