EdgeWatcher個人開発 / 設計中

使わなくなった Android を、定点観測所にする。

引き出しに眠っている端末には、カメラと GPS と通信と電源管理がひとつの筐体に収まっている。 屋外に固定して電源を挿せば、それはもう観測装置として足りている。EdgeWatcher は、 寄せ集めの Android を観測地点として登録し、送られてくる画像を1枚のシートで見続けるための仕組み。

Kotlin 端末Go APIReact 閲覧Terraform 基盤

05:00
05:05
05:10
05:15
05:20
05:25
05:30
05:35
05:40
05:45
05:50
05:55
06:00
06:05
06:10
06:15
06:20
06:25
06:30
06:35
06:40
06:45
06:50
06:55
接続中5分間隔1日 288 コマ保持 7 日

コンセプト型番は揃わない

置いた場所が観測地点で、機体はいつでも入れ替わる。

余剰端末や格安端末の寄せ集めで組む以上、同じ型番を並べることはできない。 だから EdgeWatcher は端末の機種を前提にしない。QR をスキャンすれば観測地点がひとつ増え、 壊れた端末を別の機体に置き換えても、再ペアリングすれば履歴はそのまま続く。

端末アプリ

実質2画面しかない。ペアリング前は QR スキャナ、あとは稼働状態と「画角を合わせる」だけ。撮った画像は端末では見せない。

バックエンド

常時起動するサーバは持たない。画像を受けて S3 と DynamoDB に置き、保持期間を過ぎたものは自動で消える。

Web アプリ

最初に出るのは全端末の最新スナップショット。1台を開くと、その地点の1日を遡れる。

構成3コンポーネント

画像が通る道と、人が見る道を分ける。

端末からの経路とオーナーからの経路は、認証方法も権限も別物として設計している。 画像バケットへの書き込みを持つのは端末側の関数だけで、Web 側は署名付き URL を発行する権限しか持たない。

DEVICE                             AWS                                 OWNER

Android app (Kotlin)               API Gateway (HTTP API)              Web app (React / Vite)
  Foreground Service                 |                                   S3 + CloudFront (OAC)
  + WakeLock + AlarmManager          +-- authorizer    session          |
  every 5 / 10 / 15 min              +-- device-auth   pairing          |
        |                            +-- device-api    upload           |
        |   image + location         +-- web-api  <-------- JWT --------+
        +---------------------------->      |
                                            +--> S3         images, private
                                            +--> DynamoDB   single table
                                            +--> Cognito    Web User Pool / Google SSO

Terraform --- every resource above, in both dev and prod

設計判断なぜそう作ったか

選ばなかった方の記録。

このプロジェクトで残す価値があるのは、動いているコードそのものより、 その形に落ち着くまでに何を捨てたかのほう。主なものを7つ挙げる。

定期実行に WorkManager を使わない端末 / タイマー

PeriodicWorkRequest は OS 側の制約で最短 15 分。5 分間隔の観測が作れない。常時給電の専用機として置く前提なので、Foreground Service と自前のタイマーに寄せ、常駐通知が出ることは許容する。

ただし Foreground Service だけでは足りない。CPU のサスペンドを妨げないため、画面 OFF 中にタイマーが止まる。ウェイクロックと AlarmManager を併用して初めて成立する。

端末用の Cognito User Pool は作らない認証

当初は端末側にも User Pool を置き、CUSTOM_AUTH でパスワードレス認証をする設計だった。だが Lambda Authorizer が毎リクエスト DynamoDB を引いて端末の有効性を確かめる以上、セッションの寿命管理はすでに DynamoDB 側にある。

Cognito に残るのは JWT の署名だけ。その対価が User Pool ひとつ分の管理、認証トリガー3本、ペアリング未完了時に残る孤児ユーザーでは釣り合わない。端末認証は自前のデバイスシークレットで完結させ、Cognito はオーナーの Google SSO に限定した。

QR は5分で消える。OTP は足さないペアリング

端末の追加は TransactWriteItems 1コールで、Device(PENDING)と使い捨ての PairingSession を同時に書く。スキャン時も1コールで、「PENDING かつ未期限」を条件に CONSUMED へ倒しながら Device を PAIRED にする。条件が外れれば片方だけ書かれることがない。

この QR は、オーナーが目の前の端末をセットアップしている5分間しか存在しない。先に第三者がスキャンしていればオーナー自身のペアリングが失敗して異常に気づけるため、二段階の確認は置いていない。

deviceId はスキャンより先に決める端末の同一性

Device レコードはペアリング開始の時点で作り、deviceId は以後変えない。端末を初期化しても、別の Android に置き換えても、再ペアリングすれば同じ観測地点として過去の画像履歴が繋がる。

型番の揃わない寄せ集めを前提にする以上、機体ではなく「置いた場所」に同一性を持たせる必要があった。

削除しても、すぐには消さないライフサイクル

1端末あたり保持期間内の観測レコードは最大 288 件 × 日数。DELETE のリクエストの中で消しきるのは現実的でないため、論理削除にして status = ARCHIVED へ倒し、資格情報を失効させる。実体の削除は後日まとめて行う。

それでも切断は即座に効く。Authorizer が毎リクエスト Device の status を見るので、有効期限の残ったセッショントークンでもその瞬間から拒否される。端末は 401 を受けると自分の資格情報を捨てて未ペアリング画面に戻る。

設定は端末に置かず、応答に相乗りさせる設定配信

屋外に固定した端末を操作するには、物理的にそこへ行くしかない。端末でしか変えられない設定があると運用が止まるので、端末側に設定画面を持たせない。

配信は Pull 型。定期アップロードのレスポンスに次回までの設定値を同梱する。AWS IoT Core のような新しいリソースを足さずに済み、代わりに反映が最大1送信サイクル遅れる。

環境は2つ。インフラは自動で流さない運用

個人開発でリリース列は1本しかなく、stg 本来の役割は dev がそのまま兼ねられる。1環境あたり Cognito・DynamoDB・S3×2・API Gateway・CloudFront・ACM が要る構成では、環境をひとつ減らす効果が大きい。

アプリコードは develop へのマージで dev に自動デプロイする。一方 Terraform は手動実行に留める。Cognito User Pool のようなステートフルなリソースは属性変更で置換され、登録済みのユーザーと端末が消えるため、plan を読まずに apply が走る状態を作らない。

技術使っているもの

構成要素の一覧。

観測デバイスKotlin / CameraX / Foreground Service + WakeLock + AlarmManager。オフライン時は件数と容量に上限を置いてローカルに溜め、復旧後に最新から送る
APIAPI Gateway(HTTP API)+ Lambda(Go)。認可の境界で web-api / device-auth / device-api / authorizer の4関数に分け、IAM を関数ごとに最小化する
データDynamoDB シングルテーブル。TTL で観測レコードを自動失効(dev 1日 / prod 7日)
画像S3(非公開)。Web へは署名付き URL でのみ渡し、ライフサイクルルールで同じ日数で削除する
認証オーナーは Cognito Web User Pool の Google SSO。端末はデバイスシークレット + 短命のセッショントークンを Lambda Authorizer で検証
WebReact(Vite)の SPA。S3 + CloudFront(OAC)+ ACM + Route53
基盤Terraform。state を bootstrap / shared / envs の3層に分離し、リソース名は edgewatcher-<env>-<name> で統一する
配信Web と Lambda は GitHub Actions(OIDC)。Android は Google Play の限定配信