結論

3台のホームラボ(storage / desktop / compute)を対象に、ログ収集の Vector 移行、devstack の compose 分割、Go config の一本化を一連の作業として実施した。最終的なホスト配置は以下のとおり。

  desktop.home.arpa          compute.home.arpa          storage.home.arpa
─────────────────          ─────────────────          ─────────────────
agent-gateway :8080        vLLM :8000                 Vector
NATS :4222                 llama.cpp :8001            Loki :3100
reranker :8081             PostgreSQL :5432           Prometheus :9090
LM Studio :1234            Dagster :3300              MinIO :9000
                           MLflow :5050
  

desktop は「リクエストを受けて適切なバックエンドに振り分ける」ルーティング層、compute は「推論・データ処理・実験管理」の計算層、storage は「ログ集約・メトリクス収集・オブジェクトストレージ」の永続化層。各ホストの責務が明確に分かれ、devstack の compose もこの3層に対応する形で分割された。


背景

3台構成のホームラボで agent-gateway を中心としたインフラを運用している。storage.home.arpa は 24/7 稼働のオブザーバビリティ+オブジェクトストア層(Prometheus、Loki、MinIO、各種 exporter)。desktop.home.arpa は macOS で、agent-gateway と NATS JetStream が動くゲートウェイ+メッセージング層。compute.home.arpa は GPU 推論(vLLM, llama.cpp)とデータプラットフォーム(PostgreSQL, Dagster, MLflow)を載せるオンデマンド起動のサーバー。

この構成の背景には日経225のリアルタイム予測がある。予測パイプラインにはイベントの安定した並列実行とメモリセーフな transform 処理が求められ、Vector の transform 層がその要件に合致した。クラウドに出すとレイテンシ、情報漏洩、リソースの不安定の懸念があるため、ローカルで完結することでリソース、マシンのコントロールを確保できる。全コンポーネントがコンテナ化されているため、機材さえあれば同じ構成を再現できる。

  1. ログ収集が Promtail に依存——Promtail は Loki 専用のテールエージェントで、NATS への publish や Prometheus metrics の生成といった複数出力に対応できない
  2. devstack が単一 compose——ルートに置かれた1つの podman-compose.yml が全サービスを束ね、本番のホスト分散配置と乖離していた
  3. 設定が .envrc に散在——ホスト名もポートも固定なのに環境変数で管理しており、デフォルト値との食い違いがバグの温床になっていた

フェーズ1: Promtail → Vector 移行

移行の動機

Vector は汎用的なデータパイプラインで、同じソースからの入力を Loki への転送、NATS への publish、Prometheus metrics の生成といった複数の出力に同時に流せる。将来的にインフラログを NATS 経由で Dagster のデータパイプラインに取り込む拡張も見据えると、Promtail から乗り換える合理性があった。

本番 Vector の構成

storage.home.arpa には rootful な systemd quadlet として Vector を配置済みだった。メモリ使用量 19.3M(ピーク 25.7M)と軽量で、Prometheus の scrape 対象にも追加済み。

稼働中の config は5つのソースを持つ。

  • journald——current_boot_only で現在のブートセッションのみ収集
  • file_security——alternatives.log, apport.log
  • file_apt——apt/dpkg のログ
  • syslog_udp——UDP:1514、MikroTik ルーター等からの syslog 受信
  • internal_metrics——Vector 自身のメトリクス

transform 層では journald を kernel と systemd に route で分岐させ、それぞれに job / host ラベルを付与する。kernel ログには ._TRANSPORT == "kernel" でマッチし、systemd ログにはさらに _SYSTEMD_UNIT__UID__ を抽出する。syslog は appname / severity / facility をパースしてラベル化する。

sink は2系統で、全 transform 出力を Loki に JSON エンコードで送信し、internal_metrics を Prometheus exporter(:9598)で公開する。外部に公開するポートは Prometheus scrape 用の 9598 と NATS クライアント接続用の 4222 のみとした。

devstack Vector の廃止

devstack にはもう1つの Vector がいた。NATS JetStream から telemetry.> subject を subscribe し、JSON パースしてドメイン別にルーティングする構成で、storage 側がログ収集、devstack 側がテレメトリ消費という2系統だった。

この2つの連携方針として3案を検討した。

  • A案: storage → NATS publish で telemetry.infra.* を desktop に流す
  • B案: devstack → Loki sink で NATS テレメトリも Loki に集約する
  • C案: 両方向に流して Loki にも NATS にも全データが揃う状態にする

一度 C案で実装を進めたが、「devstack は開発用なのだから本番側へ委譲すべき」という判断で方針を切り替えた。devstack の Vector は丸ごと削除し、storage 側もシンプルな構成(journald + file + syslog → Loki + Prometheus exporter)に戻した。

重要な設計判断として、NATS への publish は agent-gateway の Go コード側が担当している。gateway の goroutine がリクエスト処理の中で telemetry.*pipeline.* subject に publish し、JetStream のストリームに蓄積する。Vector はログ収集の専門であり、agent-gateway のイベントパイプラインとは別系統。CorrelationID による横串の関連付けだけで十分という結論になった。


フェーズ2: devstack のホスト単位分割

分割の動機

ルートの podman-compose.yml には NATS、PostgreSQL、Dagster(3コンテナ)、MLflow、Reranker、minio-init など計8サービスが同居していた。本番では storage / desktop / compute にサービスが分散配置されるのに、devstack は全部が1つの compose に詰まっている。「devstack で動いたが本番に持っていくと接続先が全部違う」という問題が顕在化しつつあった。

分割結果

本番のホスト配置に合わせて2つの compose に分割した。

  • devstack/desktop/ — NATS + nats-init + Reranker
  • devstack/compute/ — PostgreSQL + Dagster (x3) + MLflow + minio-init (for development)

desktop 側は軽い。NATS コンテナ(JetStream 有効、ヘルスチェック付き)、nats-init(PIPELINE / TELEMETRY ストリーム作成)、Reranker のみ。

compute 側には cross-host 参照が入る。Dagster の dagster-user-code と dagster-daemon は NATS_URL: nats://desktop.home.arpa:4222 で desktop の NATS に接続する。元々は compose 内部の nats://nats:4222 だったが、NATS が別ホストの compose に移動したのでホスト名で参照する形に変わった。compose を跨いだ依存制御はできないため、起動順序は運用で担保する。

Dagster / MLflow の配置判断

当初は「Dagster UI を desktop に置いて、user-code と daemon だけ compute に分離できないか」と検討した。Dagster はアーキテクチャ上 webserver と user-code / daemon が分離可能で、workspace.yaml で gRPC エンドポイントを指定すればリモート参照できる。

しかし MLflow は mlflow server コマンドで UI と tracking server が一体であり、分離できない。Dagster のアセットから MLflow の実験リンクを参照する場面があるため、同じホストにあるほうが運用しやすい。「UI と計算は分離できない」という現実的な制約を受け入れ、Dagster も MLflow も compute に全部置く方針を確定した。desktop からは http://compute.home.arpa:3300(Dagster UI)と :5050(MLflow UI)にブラウザでアクセスするだけで済む。

minio-init の扱い

一度削除したが復元した。storage.home.arpa に MinIO は稼働しているものの、agw-mlflow / agw-iceberg バケットがまだ未作成だった。初回セットアップ時にバケットを確実に作成する初期化ジョブとして残し、MLflow の depends_on に service_completed_successfully 条件を設定してバケットがない状態での起動を防いだ。


フェーズ3: .envrc 廃止と defaults.go への一本化

動機

agent-gateway の設定は direnv の .envrc で管理していた。COMPUTE_HOST, STORAGE_HOST, VLLM_BASE_URL, NATS_URL, POSTGRES_DSN 等25行ほどの環境変数が定義され、go run 時にシェルから注入される仕組みだった。

しかしローカル基盤ではホスト名もポートも固定であり、シークレットも開発用の固定値。環境変数で切り替える必要がない。むしろ .envrc が古くなったときに「config.go のデフォルト値と .envrc の値が食い違っている」というバグの温床になる。

設計

internal/config/defaults.go に全デフォルト値を定数として集約した。3台のホスト名、各サービスのポート番号、DB 接続情報、ログ設定のデフォルトをすべて Go の const ブロックに移動。config.goLoad() 関数は envOr / boolEnvOr / intEnvOr / optionalBoolEnv の4つのヘルパーに統一し、return 文1つで AppConfig を構築する簡潔な形にした。

核になるのは3台のホストを起点にしたサービスディスカバリのパターン。

  COMPUTE_HOST → vLLM, llama.cpp, PostgreSQL, Dagster, MLflow
STORAGE_HOST → Loki, MinIO, Prometheus
DESKTOP_HOST → NATS, Reranker, LM Studio
  

ホスト名さえ正しければ、個別の URL を環境変数で指定する必要はない。env 上書きの口は残してあるので、開発中に一時的にポートを変えたい場合は VLLM_BASE_URL=http://localhost:8000 go run ./cmd/server のようにオーバーライドできる。

実装中にポート番号の取り違えが2回起きた。llama.cpp が 8081、Reranker が 8001 になっていたが、正しくは llama.cpp:8001(compute 側)、Reranker:8081(desktop 側)。また DagsterBaseURL のデフォルトホストを当初 desktop としていたが、フェーズ2 で Dagster は compute に全部置くと決めたので compute に修正した。defaults.go のコメントにはどのホストに属するポートかを明記してある。


イベントフローの全体像

2系統のデータ経路がある。

  • Path A: agent-gateway の goroutine → NATS publish → Dagster sensor。リクエストのリネージ追跡やデータ合成に使う非リアルタイム経路
  • Path B: storage の Vector → Loki。インフラログのリアルタイム集約経路

両者は CorrelationID で横串を通せる設計になっている。Vector が NATS に流す必要がないのは、Path A がすでに agent-gateway 側で完結しているためだ。

devstack の compose 分割により、開発時も本番に近い接続構成でテストできるようになった。compute 側 Dagster の NATS_URLnats://desktop.home.arpa:4222 を向いており、本番配置と同じネットワークトポロジーで動く。全部 localhost で動かしたい場合は環境変数でオーバーライドすればよい。