デカップリングされたデプロイメント
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ビルドを配布:
-
グリーングラスが新しいコンポーネントバージョンをデプロイ → 古いTauriを停止 → 新しいTauriを開始
-
新しいTauriが直ちにクラッシュ
-
グリーングラスがコンポーネントを
BROKENと見なす -
デプロイメントポリシーに応じて、グリーングラスはロールバックするか、しないかもしれません。特にクラッシュが遅い場合(起動後、30秒実行してから終了)
-
今、デバイスは黒いX11スクリーンを表示しています
-
リモートレバーは、正しくない何かをプッシュしたばかりの同じグリーングラスデプロイメントシステムだけです
バグが 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/currentsymlink →/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
バージョン切り替えプロトコル(ローカル、高速、クラウドラウンドトリップなし)
-
コンポーネント B がダウンロードを完了 →
/opt/app/ready_markers/1.4.3.readyを書き込み -
Supervisorはマーカーを見る → 「切り替え準備完了」ヘルスイベントを公開
-
Supervisorが「1.4.3に切り替え」コマンドを受信(またはポリシーごとに自動切り替え)
-
Supervisor: Tauriに
SIGTERM(グレースフル)、待機、必要に応じてSIGKILL -
Supervisor:
ln -sfn versions/1.4.3 current(原子シンリップフリップ) -
Supervisor:
current/からTauriを開始 -
Supervisor: 60秒のウィンドウ内でハートビートを待機
-
ハートビートを受信した場合 → 切り替え成功とマーク、状態を公開
-
ハートビートなし → 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で十分以上に高速です。
-
公式
awsiotsdkPythonパッケージは保守され、十分に文書化されており、グリーングラスIPCクライアントは第一級です。 -
静的リンクの悪夢がない。
pip installin CI、venvまたはPyInstallerバンドルを配布。 -
SDKがCVEを取得する →
pip install --upgrade、小さなコンポーネントを再デプロイ。C++ツールチェーンの再ビルドなし。 -
グリーングラス自体は公式のPythonコンポーネントテンプレートと例を配布 — C++より多くの作業コード。
代わりにC++を選ぶ場合:
-
µs レベルのレイテンシ要件(これではない)
-
マイクロコントローラーで実行(これではない)
-
チームが保守する必要のある既存C++コードベース
-
ハードライセンス制約
Tinker Board 上のフロート監督のために、Pythonは厳密により少ない痛みです。
ライフサイクル責任
supervisorは以下を所有:
-
Tauriプロセスライフサイクル — fork/exec、シグナル処理、指数バックオフによる再起動
-
ウォッチドッグ — ハートビートファイル、IPC ping、「生きてるが停止している」検出(CPU固定イベントループ)
-
バージョン管理 — ポインターファイルを読む、readyマーカーを見る、原子切り替えを実行、ロールバックを処理
-
グリーングラスIPC — nucleusへの永続的なEventStream RPC接続
-
テレメトリ — デバイスシャドウアップデートを公開:アプリバージョン、Xサーバーステータス、再起動数、最後のエラー
-
コマンド処理 — MQTT トピックをサブスクライブ:「バージョン切り替え」、「アプリ再起動」、「デバイス再起動」
-
ローカルプロトコルサーバー — 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-supervisorとcom.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フロートアーキテクチャノートの付属ドキュメント。実装のランディングに更新。