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

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

Context: AWS Greengrass v2 · Tauri · Debian 12 · Tinker Board · WebKitGTK 2.40 · X11 · CPU rendering
Target scale: 1000 devices
Status: アーキテクチャ提案 — まだ実装されていません


TL;DR

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

  • Supervisor — 小さく、退屈で、めったに変わらない。Greengrass IPC 接続を所有します。Tauri アプリのライフサイクルを管理します。

  • App binary — Tauri ビルド自体。頻繁に変わります。ディスク上に存在するだけです。スーパーバイザーがいつどのように実行するかを決定します。

これを行うことの副作用:現在の Tauri アプリの bindgen-over-C++ FFI レイヤーは完全に消えます。Tauri アプリは AWS IoT SDK をまったく必要としなくなります。


解決しようとしている問題

今日の「結合」とは

Build on Tinker Board → S3 → Greengrass component pulls binary → runs Tauri app

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

障害モード

スタートアップバグのある Tauri ビルドを配送:

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

  2. 新しい Tauri が即座にクラッシュ

  3. Greengrass がコンポーネントを BROKEN としてマーク

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

  5. デバイスが黒い X11 スクリーンを表示している

  6. リモートレバーはただ1つ — 壊れたものを押したばかりの同じ Greengrass デプロイメントシステム

バグが監視者が読む設定ファイルを破損したり、GPU メモリを消費して X をくさび止めたりした場合、デバイスが不健康すぎてデプロイメントを完了できないため、修正をプッシュすることもできません。

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

二次的な障害:Tauri アプリが IPC 接続を保持

Tauri アプリ Greengrass コンポーネントであるため、Nucleus への EventStream RPC ソケットを所有しています。Tauri を再起動するたびに:

  • EventStream RPC 接続を破棄

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

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

  • ack されていないインフライトメッセージを失う

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

三次的な障害:セキュリティモデルは面倒

Greengrass IPC 認可はコンポーネント単位です。Nucleus はコンポーネントのレシピに基づいて「コンポーネント X はトピック Y に公開することが許可されているか?」をチェックします。

Tauri アプリが IPC と通信する場合、それはどのコンポーネントとして認証されているか? 3 つの通常の回避策、すべて悪い:

  • Tauri を Greengrass コンポーネントとして実行 → デカップリングと戦う

  • SVCUID をハードコード → セキュリティ問題。これらはプロセスごと、回転の対象となっています

  • Tauri を ggc_user として実行 → GUI が Greengrass システムユーザーとして実行;その権限モデルを監査したい人はいません

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


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

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

**Component A: **app-supervisor — めったに変わらない(四半期ごと)

小さく、退屈で、実戦テストされたプロセス。唯一の仕事:

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

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

  • クラッシュで再起動、指数バックオフを使用

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

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

  • ローカルポインタファイルから実行する Tauri バイナリを読み込む(例、/opt/app/current シンボリックリンク → /opt/app/versions/1.4.2/

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

**Component B: **app-binary — 頻繁に変わる(リリース単位)

実際の Tauri ビルド。その仕事はただディスク上に存在することです。

  • 自分自身を実行しない

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

  • 署名を検証

  • 「準備完了」マーカーを書き込む

  • Supervisor がマーカーを見て、切り替える時期を決定(原子的なシンボリックリンク反転)、新しいパスを指す Tauri プロセスを再起動

Component B のデプロイメントは完全に失敗する可能性があります — ダウンロードが悪い、署名が一致しない、半分書かれたファイル — ただし、スーパーバイザーは実行中、クラウドに報告中、「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 reads this
├── versions/
│ ├── 1.4.1/ # kept for rollback
│ ├── 1.4.2/ # running
│ └── 1.4.3/ # downloaded, not yet active
├── ready_markers/
│ └── 1.4.3.ready # signature-verified, ready to switch
└── supervisor/ # Component A, separate lifecycle

/run/app/
└── supervisor.sock # Unix socket: Tauri ↔ Supervisor

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

  1. Component B はダウンロード完了 → /opt/app/ready_markers/1.4.3.ready を書き込む

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

  3. Supervisor は「1.4.3 に切り替え」コマンドを受け取る(またはポリシーに従って自動切り替え)

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

  5. Supervisor: ln -sfn versions/1.4.3 current(原子的なシンボリックリンク反転)

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

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

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

  9. ハートビートなし場合 → シンボリックリンクを 1.4.2 に戻す、再起動、失敗を公開

ロールバックはローカルで数秒で行われ、クラウドラウンドトリップも新しい Greengrass デプロイメントも必要ありません。 それがデカップリングが重要な理由です。


これが 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 アプリはもはや Greengrass と通信しません。スーパーバイザーと自明なローカルプロトコルを通じて通信します:

Tauri app (Rust)
└── tokio::net::UnixStream + serde_json (~50 lines)
└── /run/app/supervisor.sock
└── Supervisor (Python) handles everything else

削除されるもの:

  • ❌ 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++ ツールチェーンに対する依存

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

それに置き換わるもの:

  • tokio::net::UnixStream

  • serde_json

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

それがトレードです。本当に脆弱なレイヤーが標準的な Rust の ~50 行になります。


スーパーバイザー:実装の選択

言語:Python

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

組み込みデバイスでは直感的ではありませんが、ここは正しいです:

  • スーパーバイザーはホットパスではありません。数秒ごとにティック、プロセスを監視、MQTT を公開、コマンドをリッスン。Python は十分に高速です。

  • 公式 awsiotsdk Python パッケージは保守され、よくドキュメント化されており、Greengrass IPC クライアントはファーストクラスです。

  • 静的リンク悪夢はありません。CI で pip install、venv または PyInstaller バンドルを配布。

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

  • Greengrass 自体は公式 Python コンポーネントテンプレートと例を配布します — C++ よりも多くの動作コード。

代わりに C++ を選ぶとき:

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

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

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

  • ハードなライセンス制約

Tinker Board 上のフリートスーパーバイザーの場合、Python は厳密により少ない痛みです。

ライフサイクル責任

スーパーバイザーは以下を所有します:

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

  2. ウォッチドッグ — ハートビートファイル、IPC ping、「動作中だが停止」検出(CPU ピンイベントループ)

  3. バージョン管理 — ポインタファイルを読み込む、準備マーカーを監視、原子的切り替えを実行、ロールバックを処理

  4. Greengrass IPC — Nucleus への永続的な EventStream RPC 接続

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

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

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

それが所有しないもの

  • anything をレンダリング

  • X11 セッションに直接タッチ(Tauri を生成し、独自の X 接続を処理)

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

  • ユーザー向け UI

スーパーバイザーは退屈であるべきです。スーパーバイザーが頻繁な更新を必要とする場合、何かがそれに忍び込んでいてそこに属していません。


ローカル 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 を使用すると、手動でそれをつつくことができます。

  • ポータブル。 Greengrass が置き換えられた場合、Tauri アプリは変わりません。スーパーバイザーが変わります。

  • デカップリングされたスキーマ。 Tauri は AWS へのワイヤとは独立して進化します。

再接続の動作

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

  • Supervisor が接続を受け入れる;Tauri の再起動に耐える

  • どちらの側も他方が活かされていると仮定しません — それらは再接続して再開します


Greengrass コンポーネントレシピ(スケッチ)

Component A: com.example.app-supervisor

RecipeFormatVersion: "2020-01-25"
ComponentName: com.example.app-supervisor
ComponentVersion: "1.0.0"
ComponentDescription: "Manages Tauri app lifecycle and Greengrass 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: "Publish device state, subscribe to commands"
operations:
- aws.greengrass#PublishToIoTCore
- aws.greengrass#SubscribeToIoTCore
resources:
- "fleet/+/state"
- "fleet/+/command"
Manifests:
- Platform:
os: linux
architecture: arm # or aarch64 for 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

Component B: com.example.app-binary

RecipeFormatVersion: "2020-01-25"
ComponentName: com.example.app-binary
ComponentVersion: "1.4.3"
ComponentDescription: "Tauri app binary. Installed on disk; supervisor decides when to run."
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}/"
# Verify signature here
sha256sum -c "${TARGET}/SHA256SUMS"
# Write ready marker — supervisor watches this directory
touch "/opt/app/ready_markers/${VERSION}.ready"
# No Run lifecycle. This component does not run anything.
Artifacts:
- URI: s3://my-bucket/app/1.4.3/app.tar.gz
Unarchive: TAR

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


Component B を配布する 2 つの方法

どちらも機能します。持つ制御量に基づいて選択してください。

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

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

  • Greengrass はシング グループターゲティング、ロールアウトリング、およびレポートを処理

  • ほとんどのチームがこれを選ぶ

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

  • アプリ更新のために Greengrass を完全にバイパス

  • より柔軟ですが、これで 2 つのフリート管理システムがあります

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

  • Greengrass が表現できない更新動作が必要な場合にのみ価値があります

Greengrass の 1000 デバイスの場合、オプション 1。 2 番目の制御プレーンを導入しないでください。


これが何をアンロック

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

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

マイグレーションパス

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

フェーズ 1:スーパーバイザーを並行して構築(本番への影響なし)

  • Python でスーパーバイザーを実装

  • Greengrass IPC に接続、テストトピックにハートビートを公開

  • 既存の Tauri アプリと並行してデバイスで実行

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

終了条件: スーパーバイザーが 72 時間クラッシュなしで実行、接続が続きます。

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

  • スーパーバイザーが Unix ソケットを開く、接続を受け入れる

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

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

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

終了条件: Tauri が 24 時間スーパーバイザー経由でハートビートを公開、直接 IPC テレメトリと一致。

フェーズ 3:ワークフローを 1 つずつ移行

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

  • 新しいパスに移行

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

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

終了条件: 各移行されたワークフローは 1 週間リグレッションなしで実行。

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

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

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

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

終了条件: C++ deps なしクリーンビルド、すべてのテストがパス。

フェーズ 5:2 つの Greengrass コンポーネントに分割

  • この時点まで、スーパーバイザーは既存のコンポーネントに配布されました

  • 今:com.example.app-supervisor および com.example.app-binary レシピに分割

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

終了条件: バージョン切り替えがカナリアで end-to-end で機能;ロールバックが機能。

フェーズ 6:フリートにロールアウト

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

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

推定総時間: 小規模なチームの場合 4~6 週間、主にコーディングではなくソーク期間でシリアライズ。


何が悪くなる可能性があるか(およびそれをキャッチする方法)

リスク緩和
スーパーバイザー自体が結合 / 複雑になる容赦なく小さく保つ。頻繁な更新が必要な場合、何か間違っている。
ローカルソケットプロトコルが RPC フレームワークに拡張されるこれに抵抗する。行区切り JSON、シンプルなメッセージタイプ。
スーパーバイザーがクラッシュして Tauri が古いデータで実行し続けるTauri はソケット接続解除を「今すぐ Greengrass がない」として扱う必要があります
原子的シンボリックリンク反転が Tauri 起動と競合Tauri はパスを読み込む必要があります(パスを前提としない)。スーパーバイザーは反転前に SIGTERM する必要があります。
準備マーカーが存在するが署名が悪い署名を検証(その後ではなく)マーカーを書き込む前に
バージョンディレクトリが永遠に蓄積されるスーパーバイザーはガベージコレクト:current + N-1 を保持、残りを削除
複数のスーパーバイザーが何らかの方法で開始される起動時 PID ファイルと flock
Tauri がブート時にスーパーバイザーに接続できないTauri はバックオフで再試行;初期ソケット欠如でクラッシュしない

未解決の質問

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

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

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

  • N-1 保持: 1 つの以前のバージョンを保持するか、3 つ? Tinker Board のディスクは制限されています。2 つで開始(current + previous)。

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


このドキュメントの範囲外

これらは完全性のため記載されていますが、独自のドキュメントに属します:

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

  • Claim による Fleet Provisioning(個別の懸念)

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

  • 可視性とメトリクス設計(スーパーバイザー後)

  • ディスク摩耗、時刻同期、シークレット管理(本番対応性ギャップ)


Greengrass + Tauri Fleet Architecture ノートのメインドキュメント付属。実装が確定したら更新してください。

Related Articles