メインコンテンツまでスキップ
最新1mo ago

デカップリングされたデプロイメント

Context: AWS Greengrass v2 · Tauri · Debian 12 · Tinker Board · WebKitGTK 2.40 · X11 · CPU rendering
Target scale: 1000デバイス
Status: アーキテクチャ提案 — 未実装


TL;DR

現在のアーキテクチャでは、Tauriアプリケーションとグリーングラス管理プレーンが単一のプロセスツリーに結合されています。これはデモ規模では機能しますが、フロート規模では失敗します。デカップリングとは、それらを2つの独立したグリーングラスコンポーネントに分割することを意味し、異なる更新ペース、異なるブラスト半径、異なるライフサイクルを持ちます:

  • Supervisor — 小さく、つまらなく、めったに変更されない。グリーングラスIPC接続を所有。Tauriアプリのライフサイクルを管理。

  • App binary — Tauri構築自体。頻繁に変更。ディスク上に存在するだけ。supervisorがいつ、どのように実行するかを決定。 この結果として、現在のTauriアプリケーション内のbindgen-over-C++ FFIレイヤーは完全に消え去ります。 Tauriアプリはもう一切AWS IoT SDKを必要としなくなります。


解決される問題

今日の「カップリング」の意味

Tinker Boardでビルド → S3 → グリーングラスコンポーネントがバイナリを取得 → Tauriアプリを実行

グリーングラスコンポーネントアプリです。1つのアーティファクト、1つのライフサイクル。更新を管理するものと更新されるものが同じプロセスツリーです。外科医が自分自身に手術をするべきではありません。

失敗モード

起動バグを持つTauriビルドを配布:

  1. グリーングラスが新しいコンポーネントバージョンをデプロイ → 古いTauriを停止 → 新しいTauriを開始

  2. 新しいTauriが直ちにクラッシュ

  3. グリーングラスがコンポーネントをBROKENと見なす

  4. デプロイメントポリシーに応じて、グリーングラスはロールバックするか、しないかもしれません。特にクラッシュが遅い場合(起動後、30秒実行してから終了)

  5. 今、デバイスは黒いX11スクリーンを表示しています

  6. リモートレバーは、正しくない何かをプッシュしたばかりの同じグリーングラスデプロイメントシステムだけです

バグが supervisorが読む設定ファイルを破損させたり、GPUメモリを消費してXをウェッジさせたりした場合、デバイスが不健康すぎてデプロイを完了できないため、修正をプッシュすることさえできないかもしれません。

デバイス数これはどのように見えるか
10デバイスSSHで接続し、手動で修正
100デバイス悪い午後
1000デバイスインシデント。人々を起こすような種類。

二次的な失敗:TauriアプリがイプC接続を保持

Tauriアプリグリーングラスコンポーネントなので、nucleusへのEventStream RPC socketを所有しています。Tauriが再起動するたびに:

  • EventStream RPC接続をティアダウン

  • nucleusで再認証(SVCUID トークン交換)

  • すべてのIPC トピックに再度購読

  • ackされていなかった進行中のメッセージを失う

冷えたWebKitキャッシュを備えたTinker Boardでは、再起動は5~15秒です。そのウィンドウ中、デバイスはフロートから見えません。テレメトリなし、コマンド受信なし、「再起動中、パニックしないでください」なし。悪いアプリバージョンで1000デバイルを掛けると、CloudWatchはフロート全体の停止を示していますが、実はUIが再起動しているだけです。

三次的な失敗:セキュリティモデルは厄介

グリーングラスIPC認可はコンポーネント単位です。nucleusは「コンポーネントXはトピックYに公開することが許可されているか?」をコンポーネントのレシピに基づいて確認します。 Tauriアプリが IPC と通信する場合、どのコンポーネントとして認証されているのですか? 3つの通常の対処法があり、すべて不適切です:

  • Tauriをグリーングラスコンポーネントとして実行 → デカップリングと矛盾

  • SVCUIDをハードコード → セキュリティの問題。これらはプロセスごと、ローテーションされるためのもの

  • Tauriをggc_userとして実行 → GUIがグリーングラスシステムユーザーとして実行。誰もその特権モデルを監査したくない

どれも清潔ではありません。それ自体がアーキテクチャがプラットフォームと争っているという信号です。


デカップリングされたアーキテクチャ

2つのグリーングラスコンポーネント、異なるライフサイクル

**コンポーネント A: **app-supervisor — めったに変更されない(おそらく四半期ごと) 小さく、つまらなく、戦闘テストされたプロセス。唯一の仕事:

  • X11セッション下でTauriアプリを子プロセスとして開始

  • ウォッチドッグ(IPC ping、またはPID + heartbeatファイルをチェック)

  • クラッシュ時に指数バックオフで再起動

  • MQTT経由でIoT Coreにヘルスを報告(alive、クラッシュN回、Xサーバーアップ/ダウン、実行中の現在のアプリバージョン)

  • 「アプリバージョンXに切り替え」コマンドをMQTTトピックでリッスン

  • どのTauriバイナリを実行するかをローカルポインターファイルから読み込む(例:/opt/app/current symlink → /opt/app/versions/1.4.2/

これが管理プレーンです。めったに更新されません。更新する場合は、注意深く実行されます。

**コンポーネント B: **app-binary — 頻繁に変更(リリースごと)

実際のTauri構築。その仕事は単にディスク上に存在することです。

  • 自分自身では実行されない

  • /opt/app/versions/1.4.3/のようなバージョン管理されたパスにダウンロード

  • 署名を検証

  • 「ready」マーカーを書き込む

  • Supervisorがマーカーを見て、いつ切り替えるか(原子シンリップフリップ)を決定し、新しいパスを指すTauriプロセスを再起動

コンポーネントBのデプロイは完全に失敗する可能性があります — ダウンロード不良、署名不一致、半分書き込まれたファイル — そしてsupervisorはまだ実行されており、まだクラウドにレポートされており、「1.4.2にロールバック」コマンドを受け取ることができます。

プロセスモデル

┌─────────────────────────────────────────────────────────┐
│ Tinker Board (Debian 12) │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Tauri app │ JSON │ Supervisor (Python) │ │
│ │ (X11 session) │◄───────►│ - Greengrass IPC │ │
│ │ - WebKitGTK 2.40 │ Unix │ - Watchdog Tauri │ │
│ │ - CPU render │ socket │ - Version switching │ │
│ │ - No IPC code │ │ - Health to MQTT │ │
│ └──────────────────┘ └──────────┬───────────┘ │
│ │ │
│ │ EventStream │
│ │ RPC │
│ ┌─────────▼───────────┐ │
│ │ Greengrass Nucleus │ │
│ └─────────┬───────────┘ │
└──────────────────────────────────────────┼──────────────┘

│ MQTT/TLS

┌─────────────┐
│ AWS IoT │
│ Core │
└─────────────┘

デバイス上のファイルレイアウト

/opt/app/
├── current → versions/1.4.2 # symlink、supervisorがこれを読む
├── versions/
│ ├── 1.4.1/ # ロールバック用に保持
│ ├── 1.4.2/ # 実行中
│ └── 1.4.3/ # ダウンロード済み、未アクティブ
├── ready_markers/
│ └── 1.4.3.ready # 署名検証済み、切り替え準備完了
└── supervisor/ # コンポーネント A、個別ライフサイクル

/run/app/
└── supervisor.sock # Unixソケット:Tauri ↔ Supervisor

バージョン切り替えプロトコル(ローカル、高速、クラウドラウンドトリップなし)

  1. コンポーネント B がダウンロードを完了 → /opt/app/ready_markers/1.4.3.ready を書き込み

  2. Supervisorはマーカーを見る → 「切り替え準備完了」ヘルスイベントを公開

  3. Supervisorが「1.4.3に切り替え」コマンドを受信(またはポリシーごとに自動切り替え)

  4. Supervisor: TauriにSIGTERM(グレースフル)、待機、必要に応じてSIGKILL

  5. Supervisor: ln -sfn versions/1.4.3 current (原子シンリップフリップ)

  6. Supervisor: current/ からTauriを開始

  7. Supervisor: 60秒のウィンドウ内でハートビートを待機

  8. ハートビートを受信した場合 → 切り替え成功とマーク、状態を公開

  9. ハートビートなし → symlink を1.4.2に戻す、再起動、失敗を公開

ロールバックはクラウドラウンドトリップやグリーングラスデプロイメントなしにローカルで数秒内に発生します。それがカップリング解除の重要なことです。


これがbindgen問題を削除する理由

現在のTauriアプリはこのスタックを持っています:

Tauri app (Rust)
└── thin Rust FFI layer (bindgen)
└── C++ AWS IoT SDK (statically linked on Tinker Board)
└── Unix socket → Greengrass nucleus IPC

デカップリングされたアーキテクチャでは、Tauriアプリはもはやグリーングラスと通信しません。supervisorと簡単なローカルプロトコル経由で通信します:

Tauri app (Rust)
└── tokio::net::UnixStream + serde_json (~50行)
└── /run/app/supervisor.sock
└── Supervisor (Python) が他のすべてを処理

削除されるもの:

  • ❌ C++ヘッダーのbindgen生成バインディング

  • ❌ 手書きCシムレイヤー(ある場合)

  • ❌ aws-crt-cpp、aws-c-mqtt、aws-c-io、aws-c-cal、aws-c-common、s2n-tls、libcryptoの静的リンク

  • ❌ FFI例外処理(とにかくUBだった)

  • std::shared_ptr / std::function クロスFFI回避

  • ❌ ビルド時のC++ツールチェーンへの依存(Tauriビルド内)

  • ❌ armv7でのアーキテクチャ固有リンク問題

置き換わるもの:

  • tokio::net::UnixStream

  • serde_json

  • ✅ ローカルIPCメッセージのスキーマ

それがトレードオフです。本当にもろいレイヤーが~50行の標準Rustになります。


Supervisor:実装の選択肢

言語:Python

推奨:公式の**awsiotsdk**パッケージを使用したPython。

組み込みデバイスで反直感的ですが、ここではそれが正しい:

  • supervisorはホットパスではありません。数秒ごとにティックし、プロセスを見守り、MQTTを公開し、コマンドをリッスン。Pythonで十分以上に高速です。

  • 公式awsiotsdk Pythonパッケージは保守され、十分に文書化されており、グリーングラスIPCクライアントは第一級です。

  • 静的リンクの悪夢がない。pip install in CI、venvまたはPyInstallerバンドルを配布。

  • SDKがCVEを取得する → pip install --upgrade、小さなコンポーネントを再デプロイ。C++ツールチェーンの再ビルドなし。

  • グリーングラス自体は公式のPythonコンポーネントテンプレートと例を配布 — C++より多くの作業コード。

代わりにC++を選ぶ場合:

  • µs レベルのレイテンシ要件(これではない)

  • マイクロコントローラーで実行(これではない)

  • チームが保守する必要のある既存C++コードベース

  • ハードライセンス制約

Tinker Board 上のフロート監督のために、Pythonは厳密により少ない痛みです。

ライフサイクル責任

supervisorは以下を所有:

  1. Tauriプロセスライフサイクル — fork/exec、シグナル処理、指数バックオフによる再起動

  2. ウォッチドッグ — ハートビートファイル、IPC ping、「生きてるが停止している」検出(CPU固定イベントループ)

  3. バージョン管理 — ポインターファイルを読む、readyマーカーを見る、原子切り替えを実行、ロールバックを処理

  4. グリーングラスIPC — nucleusへの永続的なEventStream RPC接続

  5. テレメトリ — デバイスシャドウアップデートを公開:アプリバージョン、Xサーバーステータス、再起動数、最後のエラー

  6. コマンド処理 — MQTT トピックをサブスクライブ:「バージョン切り替え」、「アプリ再起動」、「デバイス再起動」

  7. ローカルプロトコルサーバー — Tauriがイベントを送信/コマンドを受信するためのUnixソケット

これが所有しないもの

  • 何かをレンダリング

  • X11セッションに直接触る(Tauriを起動し、独自のX接続を処理)

  • アプリケーションレベルのビジネスロジック

  • ユーザー向けUI

supervisorはつまらなくあるべき。supervisorが頻繁に更新を必要とする場合、そこに属さないものがそこに忍び込んでいます。


ローカルIPCプロトコル(Tauri ↔ Supervisor)

トランスポート

/run/app/supervisor.sock でのUnixドメインソケット。X11セッションユーザーが読み書きできるようにアクセス許可を設定。

ワイヤ形式

行区切りJSON。1行につき1つのJSONオブジェクト。フレーミングプロトコルは不要。

方向:Tauri → Supervisor(イベント)

{"type": "heartbeat", "ts": 1715800000, "version": "1.4.2"}
{"type": "event", "name": "button_pressed", "payload": {"button_id": "start"}}
{"type": "telemetry", "metrics": {"frame_time_ms": 42, "memory_mb": 187}}
{"type": "error", "level": "warn", "message": "websocket disconnected"}

方向:Supervisor → Tauri(コマンド)

{"type": "command", "name": "reload_config"}
{"type": "command", "name": "shutdown", "reason": "version_switch"}
{"type": "config_update", "config": {...}}

このプロトコルが自明に優れている理由

  • Tauriアプリ内でSDK依存性がない。 ただのtokio::net::UnixStream + serde_json

  • テストが簡単。 nc -U /run/app/supervisor.sock を使うと手動でそれを突くことができます。

  • ポータブル。 グリーングラスが置き換えられたとしても、Tauriアプリは変更されません。supervisorが変更します。

  • スキーマのデカップリング。 Tauriは独立して進化し、ワイヤに何があるかは関係なく。

再接続動作

  • Tauriは切断時に指数バックオフでソケットに再接続

  • Supervisorは接続を受け入れ、Tauri再起動を許容

  • どちらの側も他方が生きていることを想定しない — 再接続して再開


グリーングラスコンポーネントレシピ(スケッチ)

コンポーネント A:com.example.app-supervisor

RecipeFormatVersion: "2020-01-25"
ComponentName: com.example.app-supervisor
ComponentVersion: "1.0.0"
ComponentDescription: "TauriアプリライフサイクルとグリーングラスIPCを管理。"
ComponentPublisher: Example Co.
ComponentDependencies:
aws.greengrass.Nucleus:
VersionRequirement: ">=2.0.0"
ComponentConfiguration:
DefaultConfiguration:
accessControl:
aws.greengrass.ipc.mqttproxy:
com.example.app-supervisor:pubsub:1:
policyDescription: "デバイス状態を発行、コマンドをサブスクライブ"
operations:
- aws.greengrass#PublishToIoTCore
- aws.greengrass#SubscribeToIoTCore
resources:
- "fleet/+/state"
- "fleet/+/command"
Manifests:
- Platform:
os: linux
architecture: arm # または aarch64(RK3399向け)
Lifecycle:
Install:
Script: |
mkdir -p /opt/app/versions /opt/app/ready_markers /run/app
chmod 755 /opt/app
Run:
Script: |
exec /opt/app/supervisor/bin/supervisor --socket /run/app/supervisor.sock
Artifacts:
- URI: s3://my-bucket/supervisor/1.0.0/supervisor.tar.gz
Unarchive: TAR
Permission:
Read: ALL
Execute: OWNER

コンポーネント B:com.example.app-binary

RecipeFormatVersion: "2020-01-25"
ComponentName: com.example.app-binary
ComponentVersion: "1.4.3"
ComponentDescription: "Tauriアプリバイナリ。ディスクにインストール。supervisorが実行時期を決定。"
ComponentPublisher: Example Co.
ComponentDependencies:
com.example.app-supervisor:
VersionRequirement: ">=1.0.0"
Manifests:
- Platform:
os: linux
architecture: arm
Lifecycle:
Install:
Script: |
VERSION="1.4.3"
TARGET="/opt/app/versions/${VERSION}"
mkdir -p "${TARGET}"
cp -r {artifacts:path}/* "${TARGET}/"
# ここで署名を検証
sha256sum -c "${TARGET}/SHA256SUMS"
# readyマーカーを書き込む — supervisorはこのディレクトリを見守る
touch "/opt/app/ready_markers/${VERSION}.ready"
# Runライフサイクルなし。このコンポーネントは何も実行しない。
Artifacts:
- URI: s3://my-bucket/app/1.4.3/app.tar.gz
Unarchive: TAR

注意:コンポーネント B は**Runセクションがありません。** これがポイントです。ファイルをインストールして終了します。


コンポーネント B を配布する2つの方法

どちらも機能します。必要なコントロール量に基づいて選択。

オプション1:ダウンローダーとしてのグリーングラスコンポーネント(推奨)

  • コンポーネントアーティファクトは小さい — S3から実際のバイナリをプルし、署名を検証し、/opt/app/versions/\\<v>/ に置き、readyマーカーを書き込むスクリプト

  • グリーングラスはシング グループターゲティング、ロールアウトリング、レポートを処理

  • ほとんどのチームはこれを選択

オプション2:署名されたS3/CloudFrontマニフェストを指すTauriの組み込みアップデーター

  • グリーングラスを完全にバイパス(アプリ更新用)

  • より柔軟ですが、今度は2つのフロート管理システムを持っています

  • 独自のロールアウトリングロジックを構築する必要があります

  • グリーングラスが表現できない更新動作が必要な場合にのみ価値あり

1000デバイスが既にグリーングラス上にある場合、オプション1。 2番目の制御プレーンを導入しないでください。


これがアンロックするもの

デカップリングの直接的な結果:

利点メカニズム
数秒でのローカルロールバックSupervisorはクラウドラウンドトリップなしでsymlink をフリップ
管理プレーンは悪いアプリデプロイから生き残るSupervisorは異なるプロセス、異なるコンポーネント
Tauri再起動はIPC接続をまばたきしないSupervisorが接続を永続的に保持
bindgen / C++ FFI削除TauriアプリはもはやAWS SDKと通信しない
独立した更新ペースSupervisorは四半期ごとに更新、アプリは週ごとに更新
より清潔なセキュリティモデルSupervisorは認証されたコンポーネント。Tauriはローカルクライアント
WebKit固定イベントループのウォッチドッグSupervisor(個別プロセス)は「生きてるがハートビートなし」を見ることができ
CPU予算分離Supervisorはnice'd/レンダリングとは異なるコアに固定可能
ビルド複雑性の削減Tauriビルドは通常のRustビルド、C++ツールチェーンなし

マイグレーションパス

現在のアーキテクチャはデモで機能しているとします。マイグレーションは段階的です。

フェーズ1:supervisorを並行して構築(本番への影響なし)

  • Pythonでsupervisor を実装

  • グリーングラスIPCに接続、テストトピックにハートビートを公開

  • devデバイス上の既存Tauriアプリと共に実行

  • 検証:接続が持続、MQTTラウンドトリップが機能、リソース問題なし

終了基準: supervisor が72時間クラッシュなしで実行、接続は保つ。

フェーズ2:ローカルソケットプロトコルを追加

  • Supervisorが Unix socket を開く、接続を受け入れる

  • Tauriアプリを修正:ソケットに接続する新しいクライアントを追加、ハートビートを送信

  • まだbindgenレイヤーを削除しないでください。 両方が並行して実行。

  • 比較:新しいパスと古いパスはテレメトリに同意していますか?

終了基準: Tauriはsupervisor 経由でハートビートを24時間公開、直接IPCテレメトリと一致。

フェーズ3:ワークフローを1つずつマイグレーション

  • 最小のIPCユースケースを選択(例:1つのアウトバウンドイベント)

  • 新しいパスに移行

  • bindgenレイヤーからその特定のコードを削除

  • ワークフローごとに繰り返す

終了基準: 各マイグレーション済みワークフローは1週間問題なく実行。

フェーズ4:bindgenレイヤーを完全に削除

  • すべてのワークフローが移行されたら、TauriからC++ FFIコードを削除

  • ビルドから静的リンクを削除

  • Tauriビルドは今、純粋なRustビルド

終了基準: C++依存なしで清潔なビルド、すべてのテスト合格。

フェーズ5:2つのグリーングラスコンポーネントに分割

  • これまでのところ supervisor は既存コンポーネントに配布されているかもしれません

  • 今:com.example.app-supervisorcom.example.app-binary レシピに分割

  • まずカナリグループにデプロイ

終了基準: バージョン切り替えはエンドツーエンドでカナリで機能、ロールバックが機能。

フェーズ6:フロートへのロールアウト

  • カナリ(5デバイス)→ Early(50) → 本番(残り)

  • 各リング間に24時間のソーク

推定総時間: 小さなチーム向け4~6週間、主にコーディングではなくソーク期間でシリアル化。


何が問題になる可能性があるか(そしてどのようにキャッチするか)

リスク軽減
Supervisor自体がカップリング/複雑になる残忍なほど小さく保つ。頻繁な更新が必要な場合、何かが間違っています。
ローカルソケットプロトコルがRPCフレームワークに成長これに抵抗。行区切りJSON、シンプルなメッセージタイプ。
SupervisorがクラッシュしてTauriが古いデータを実行し続けるTauriはソケット切断を「今グリーングラスなし」として扱う必要があり、グレースフルに低下
原子symlink フリップがTauri起動と競合Tauriは起動時にsymlink を読む必要があり、パスを想定しない。Supervisorはフリップ前にSIGTERMする必要があり。
Readyマーカーが存在するが署名が悪いマーカーを書き込むに署名を検証、後ではなく
バージョンディレクトリが永遠に蓄積Supervisorガベージコレクション:current + N-1を保持、残りを削除
複数の supervisor が何らかの形で起動起動時のPIDファイルとflock
Tauriは起動時にsupervisor に接続できないTauriはバックオフで再試行。初期ソケット不在でクラッシュしない

オープンな質問

  • Supervisor パッケージング形式: raw Python + venv、またはPyInstaller単一バイナリ? PyInstallerはバンドルサイズを追加しますが、デプロイメントを簡素化。おそらくPyInstaller。

  • ハートビート頻度: 5秒ごと? 30秒ごと? トレードオフ:検出レイテンシ vs. ログノイズ。10秒から開始。

  • ロールバックポリシー: N回のハートビート失敗後に自動ロールバック、またはクラウドからの明示的なコマンドが必要? 安全性のために自動ロールバック推奨、監査ログをIoT Coreに。

  • N-1保持: 1つの前のバージョンを保持、または3つ? Tinker Board上のディスクは限定的。2(current + previous)から開始。

  • Supervisor → Tauri コマンド認可: Tauriはソケットからすべてを信頼していますか? おそらくはい、ソケットはファイルシステム許可保護されているため。


このドキュメントの対象外

完全性のために言及されていますが、独自のドックに属します:

  • CI/クロスコンパイルパイプライン(前提条件、メインアーキテクチャドキュメント参照)

  • クレームによるフロートプロビジョニング(別の懸念事項)

  • シング グループロールアウトリング(デプロイメントポリシー、アーキテクチャではなく)

  • 可観測性とメトリクス設計(post-supervisor)

  • ディスク摩耗、時刻同期、秘密管理(本番環境準備ギャップ)


メインのグリーングラス + Tauriフロートアーキテクチャノートの付属ドキュメント。実装のランディングに更新。

Related Articles