## Deployer / NodeAgent / mTLSサービスメッシュ設計(ベアメタル向け) 本書では、既存の `deployer/` クレート群と `baremetal/first-boot` を土台に、 Chainfire を「ソース・オブ・トゥルース」とした常駐型 Deployer / NodeAgent およびサービスメッシュ風 mTLS Agent の設計を定義する。 既存の first-boot は **初回クラスタ参加と基本サービス起動** に特化した Bootstrapper として残し、本設計では **その後のライフサイクル管理と サービス間 mTLS 通信** を担うコンポーネントを追加する。 --- ### 1. Chainfire 上の論理モデル Chainfire は etcd 互換の KV ストアとして既に存在するため、 以下のような論理モデルを「キー空間+JSON 値」として表現する。 #### 1.1 ネームスペースとキー空間 - `photoncloud/` - `clusters/{cluster_id}/nodes/{node_id}` - `clusters/{cluster_id}/services/{service_name}` - `clusters/{cluster_id}/instances/{service_name}/{instance_id}` - `clusters/{cluster_id}/mtls/policies/{policy_id}` - `clusters/{cluster_id}/mtls/certs/{node_id or service_name}` `deployer` 既存の Chainfire 利用は `namespace = "deployer"` だが、 クラスタ状態については今後 `photoncloud/` 名前空間を利用する。 Deployer 内部状態(phone-home の登録情報など)は引き続き `deployer/` 名前空間でもよい。 #### 1.2 Cluster / Node モデル - **Cluster**(メタ情報のみ) - キー: `photoncloud/clusters/{cluster_id}/meta` - 値(JSON): - `cluster_id: string` - `environment: "dev" | "stg" | "prod" | "test"` - `created_at: RFC3339` - **Node** - キー: `photoncloud/clusters/{cluster_id}/nodes/{node_id}` - 値(JSON): - `node_id: string` - `ip: string` - `hostname: string` - `roles: string[]` 例: `["control-plane"]`, `["worker"]` - `state: "pending" | "provisioning" | "active" | "failed" | "draining"` - `labels: { [key: string]: string }` - `last_heartbeat: RFC3339` - `machine_id: string` この `Node` は既存の `deployer_types::NodeInfo` と 1:1 でマッピング可能にする。 #### 1.3 Service / ServiceInstance モデル - **Service** - キー: `photoncloud/clusters/{cluster_id}/services/{service_name}` - 値(JSON): - `name: string` 例: `"chainfire"`, `"flaredb"`, `"iam"`, `"apigateway"` - `ports: { "http"?: number, "grpc"?: number }` - `protocol: "http" | "grpc"` - `mtls_required: boolean` - `mesh_mode: "agent" | "none"` - `metadata: { [key: string]: string }` - **ServiceInstance** - キー: - `photoncloud/clusters/{cluster_id}/instances/{service_name}/{instance_id}` - 値(JSON): - `instance_id: string` - `service: string` - `node_id: string` - `ip: string` - `port: number` // アプリケーションのローカルポート (例: 127.0.0.1:9000) - `mesh_port: number` // mTLS Agent が listen するポート (例: 10.0.x.x:10443) - `state: "starting" | "ready" | "unhealthy" | "draining" | "gone"` - `version: string` - `registered_at: RFC3339` - `last_heartbeat: RFC3339` ServiceInstance 情報は、将来的に mTLS Agent からも watch され、 サービス発見とロードバランシングの元データとなる。 #### 1.4 mTLS Policy モデル - **MTLSPolicy** - キー: - `photoncloud/clusters/{cluster_id}/mtls/policies/{policy_id}` - 値(JSON): - `policy_id: string` - `environment: string` // 例: "dev", "stg", "prod" - `source_service: string | "*"` // 例: "apigateway" or "*" - `target_service: string | "*"` - `mtls_required: boolean` - `mode: "strict" | "permissive" | "disabled"` - `updated_at: RFC3339` 解決アルゴリズム(例): 1. `source_service`, `target_service` 完全一致のポリシーを検索。 2. `source_service="*"` or `target_service="*"` のワイルドカードポリシーを fallback として適用。 3. どれもなければ、Cluster の `environment` ごとのデフォルト: - `dev`: `mtls_required=false` - `stg/prod`: `mtls_required=true` mTLS Agent は「自サービス名」と「接続先サービス名」をもとに、 Chainfire からこのポリシーを解決し、mTLS/TLS/平文を切り替える。 #### 1.5 証明書メタデータ - **CertificateBinding** - キー: - `photoncloud/clusters/{cluster_id}/mtls/certs/services/{service_name}` - `photoncloud/clusters/{cluster_id}/mtls/certs/nodes/{node_id}` - 値(JSON): - `subject: string` // "spiffe://photoncloud/{cluster_id}/service/{service_name}" 等 - `ca_id: string` - `expires_at: RFC3339` - `last_rotated_at: RFC3339` - `fingerprint_sha256: string` 実際の鍵/証明書 PEM は Chainfire には保存せず、 別の Secret ストア or Deployer 専用ストレージ(現行の `PhoneHomeResponse` 等) で管理する想定とする。 --- ### 2. Deployer / NodeAgent の責務とインターフェース #### 2.1 Deployer(中央)の責務 - `deployer-types` で定義されている `NodeInfo` / `NodeConfig` を、 上記 `Node` モデルと 1:1 でマッピングして Chainfire に保存する。 - 管理 API からのリクエスト(ノード登録、ロール更新など)を受けて、 `photoncloud/clusters/{cluster_id}/nodes/{node_id}` を更新する。 - 「どの Node にどの Service を何インスタンス置くか」を決め、 `ServiceInstance` エントリを Desired State として作成する。 #### 2.2 NodeAgent(各ノード)の責務 NodeAgent は各ノード上の常駐プロセスとして動作し、**宣言的な Desired State** (Chainfire 上の ServiceInstance 等)と、実ノードの **Observed State** (systemd プロセス・mTLS Agent・ローカルポート)を Reconcile する。 ##### 2.2.1 NodeAgent の外部インターフェース - **入力** - Chainfire: - `nodes/{node_id}`(自ノード) - `instances/*`(自ノードに紐づくインスタンス) - `mtls/policies/*`(mTLS 設定) - ローカル: - systemd / プロセスリスト - ローカル設定ファイル(例: `/etc/photoncloud/node-agent.toml`) - **出力** - Chainfire: - `nodes/{node_id}.last_heartbeat` - `instances/{service}/{instance_id}.state` - `instances/{service}/{instance_id}.last_heartbeat` - ローカル: - systemd unit の起動/停止(アプリケーションサービス+mTLS Agent) - ローカル設定ファイル生成(サービス別 config, cert 配置 など) ##### 2.2.2 Reconcile ループ(擬似コード) NodeAgent 内部のメインループイメージ: ```text loop every N seconds or on Chainfire watch event: desired_instances = chainfire.get_instances_for_node(node_id) observed_instances = local.inspect_processes_and_ports() # 1. 起動すべきインスタンス for each instance in desired_instances: if !observed_instances.contains(instance): start_app_service(instance) start_mtls_agent(instance) # 2. 停止すべきインスタンス for each instance in observed_instances: if !desired_instances.contains(instance): stop_mtls_agent(instance) stop_app_service(instance) # 3. 状態更新 update_heartbeats_and_state_in_chainfire() ``` 実際の実装では、Chainfire の watch 機構を使い、イベント駆動+バックオフを行う。 ##### 2.2.3 Deployer との関係 - Deployer は「どの Node にどの ServiceInstance を置くか」を決めて Chainfire に書き込む **コントロールプレーン**。 - NodeAgent は、それを読んでローカルマシンを Reconcile する **データプレーン**。 - 将来的には、NodeAgent も Chainfire の `ClusterEventHandler` を実装し、 gossip/メンバーシップと連動させることもできる。 #### 2.3 mTLS Agent からの利用 mTLS Agent は、以下の用途で Chainfire を参照する: - **サービス発見** - `instances/{service_name}/` プレフィックスを Range/Watch し、 - `state = "ready"` のインスタンス一覧をキャッシュ。 - ローカルノード / リモートノードにまたがるインスタンスをロードバランス。 - **ポリシー取得** - `mtls/policies/` を Range/Watch してローカルキャッシュ。 - 接続時に `(source_service, target_service)` から mTLS モードを決定。 - **証明書メタ情報** - 自身のサービス/ノードに対応する CertificateBinding を取得し、 ローテーションのタイミングや失効を検知。 --- ### 3. mTLS オン/オフ制御と環境別デフォルト #### 3.1 環境ごとのデフォルト Cluster メタデータに `environment` を持たせ、以下のポリシーを推奨とする: - `environment = "dev"`: - デフォルト: `mtls_required = false` - Chainfire 上で `MTLSPolicy` を設定すれば、特定サービス間のみ mTLS を有効化可能。 - `environment = "stg" | "prod"`: - デフォルト: `mtls_required = true` - 明示的な `mode = "disabled"` ポリシーでのみ例外を許可。 #### 3.2 エージェント側の実装フラグ mTLS Agent 側では、以下の 2 層で制御する: - **コンパイル/起動時フラグ** - `MTLS_FORCE_DISABLED=true` などの環境変数で完全無効化(テスト用)。 - **Chainfire ポリシー** - ランタイムで `MTLSPolicy` を変更することで、 - 通常は mTLS - 一時的に plain を切り替え。 NodeAgent は Cluster/Node の `environment` と `labels` を参照し、 開発用の「完全プライベート環境」ではデフォルトで mTLS Agent を 平文モードで起動するなどの戦略も取れる。 --- ### 4. mTLS Agent(Sidecar)の役割と API #### 4.1 役割 - 各サービスの横で動作する小さなプロキシプロセス。 - アプリケーションは常に **`127.0.0.1:`** で平文 HTTP/gRPC を喋る。 - mTLS Agent は以下を担当する: - 外部からの受信: `0.0.0.0:` で mTLS/平文を受け、`127.0.0.1:` にフォワード。 - 外部への送信: Chainfire の ServiceInstance/MTLSPolicy を参照して、接続先と mTLS モードを解決し、上流へ接続。 #### 4.2 ローカル設定ファイル例 NodeAgent から生成される設定ファイル(例: `/etc/photoncloud/mtls-agent.d/{service}.toml`): ```toml [service] name = "creditservice" app_addr = "127.0.0.1:9100" mesh_bind_addr = "0.0.0.0:19100" [cluster] cluster_id = "prod-cluster" environment = "prod" chainfire_endpoint = "https://chainfire.local:2379" [mtls] mode = "auto" # "auto" | "mtls" | "tls" | "plain" ca_cert_path = "/etc/nixos/secrets/ca.crt" cert_path = "/etc/nixos/secrets/creditservice.crt" key_path = "/etc/nixos/secrets/creditservice.key" ``` #### 4.3 mTLS オン/オフと動作モード 動作モードは以下の 4 種類を想定する: - `mtls`: 双方向 TLS(クライアント証明書必須) - `tls`: 片方向 TLS(サーバ証明書のみ) - `plain`: 平文 HTTP/gRPC - `auto`: Chainfire の `MTLSPolicy` を参照して上記 3 つから選択 mTLS Agent の起動引数(例): ```bash mtls-agent \ --config /etc/photoncloud/mtls-agent.d/creditservice.toml \ --default-mode auto ``` - dev 環境では NodeAgent が `default-mode=plain` で起動する運用も可能。 - stg/prod では `default-mode=mtls` とし、ポリシーで例外を作る。 #### 4.4 クライアント側 API(アプリから見た抽象) アプリケーションコード側では、`client-common` 等に薄い抽象を用意する: ```rust /// 論理サービス名ベースで呼び出す HTTP クライアント pub async fn call_service( svc: &str, path: &str, body: Option>, ) -> Result>, Error> { // 実装イメージ: // 1. ローカル mTLS Agent の「コントロールポート」(例: 127.0.0.1:19080) に // 「svc=creditservice, path=/v1/foo」等を投げる // 2. Agent が Chainfire を参照して適切な backend を選択 // 3. mTLS/TLS/平文は Agent 側で判断 unimplemented!() } ``` 最初の段階では、シンプルに「アプリは `http://127.0.0.1:` に対して proxy 経由で呼び出す」形でもよい。 --- ### 5. 既存サービスの移行方針(概要) - 既存のサーバー(`iam-server`, `creditservice-server`, など)は、 まずは **localhost で平文待受** に統一する(必要な範囲から徐々に)。 - Deployer/NodeAgent は、サービス起動時に mTLS Agent を隣に起動し、 Chainfire 上の Service/ServiceInstance 情報を更新する。 - クライアント側は、徐々に `client-common` ベースの論理サービス名呼び出しに 置き換え、最終的に mTLS の有無をアプリから隠蔽する。 --- ### 4. 今後の実装ステップへのブレークダウン 本仕様に基づき、今後は以下を実装していく: 1. `chainfire-client` を用いた小さなユーティリティ(例: `photoncloud-ctl`)で、 上記キー空間の CRUD を行う PoC を作成。 2. `deployer-server` から Chainfire への書き込み/読み出しコードを追加し、 既存の in-memory/local-file backend と並行して動かす。 3. NodeAgent プロセス(新クレート or 既存 `plasmacloud-reconciler` 拡張)を実装し、 1 ノード内での ServiceInstance Reconcile ループを構築。 4. 別クレートとして mTLS Agent の最小実装(plain proxy モード)を追加し、 ServiceInstance モデルと連動させる。 これにより、Kubernetes なしのベアメタル環境であっても、 Chainfire を中心とした「宣言的なクラスタ状態管理+サービスメッシュ風 mTLS」 を段階的に実現できる。