
クラウドからのデバイス制御が発生するIoT システムを運用していくうえで、もっとも厄介なテーマのひとつが「クラウドとデバイスの状態をいかに矛盾なく保つか」という同期問題です。IoTデバイスはネットワーク的に不安定で、再接続のたびに状態が食い違う可能性があります。その同期処理を JSON ベースで肩代わりしてくれる仕組みが、AWS IoT Core の Device Shadow です。
本記事では、そのDevice Shadowを使用する上でハマる落とし穴について具体例と解決策を紹介していきます。
▼ 本記事の対象読者
- AWS IoT Coreを使用して、デバイスの遠隔制御機能を構築しようとしているエンジニア
- 現場の運用・保守にIoT導入を検討されている方
- サービスの仕様を策定し、ユーザー体験を設計する責任者
▼ 本記事で取り扱わないこと
- AWS IoT Core 自体の基本的なセットアップや接続手順
- Device Shadow の名前付きシャドウとクラシックシャドウの違いや詳細な使い方
- デバイス側のファームウェア実装などの具体的なコード例
Device Shadow の基本構造とフロー
まずはDevice Shadow という機能について簡単に説明します。
Device Shadow は、デバイスごとにクラウド側で保持される JSON ドキュメントです。中心になるのは次の三つの要素です。
desired:クラウド側が「こうなってほしい」と宣言する目標状態reported:デバイス側が「いま自分はこうなっている」と申告する実状態delta:desiredとreportedの差分。ブローカー側で自動計算される
代表的な Shadow ドキュメントは次のようなイメージです。
{
"state": {
"desired": {
"led": "off",
"color": "green",
"interval": 30
},
"reported": {
"led": "off",
"color": "green",
"interval": 30
}
}
}ここから例えば、desired の led を on に変更するとブローカーがそれを検知して delta トピックに以下のような差分がパブリッシュされます。
{
"version": 6,
"timestamp": 1777345874,
"state": {
"led": "on"
},
"metadata": {
"led": {
"timestamp": 1777345874
}
}
}これによってデバイスは delta トピック経由で、「led を on にせよ」という指示を受け取れます。デバイス側は反映後に対になる reported.led を on に更新することで、desired と reported が一致し delta は消えます。
フロー図に起こすと以下のようなイメージです。

これが基本的なデバイスシャドウの更新の流れです。
Shadow 関連トピックの全体像
Device Shadow は MQTT トピックを通じて操作を行うことができます。
つまり、トピックの役割を体系的に把握しておくと仕組みの理解や運用設計が楽になります。
名前付きシャドウの場合、$aws/things/<thingName>/shadow/name/<shadowName> がベースとなり、その配下に同じ操作群が並びます。
| 操作 | publish 先 | 受信トピック(成功/失敗) | 主な用途 |
|---|---|---|---|
| 取得 | .../shadow/get | .../shadow/get/accepted / .../shadow/get/rejected | 起動直後の現在状態取得 |
| 更新 | .../shadow/update | .../shadow/update/accepted / .../shadow/update/rejected | desired/reported の書き込み |
| 差分通知 | (受信のみ) | .../shadow/update/delta | desired と reported の差分検知 |
| ドキュメント全体通知 | (受信のみ) | .../shadow/update/documents | 更新前後のスナップショット |
| 削除 | .../shadow/delete | .../shadow/delete/accepted / .../shadow/delete/rejected | Shadow ドキュメントの削除 |
基本フローの落とし穴
ここでやっと本題です。先ほど紹介した基本の運用フローで一見問題ないように感じますが、私の場合は実務のフローに当てはめていくとある問題が発生することがわかりました。例えば以下のような例です。
「現場の判断でLEDライトのカラーがgreenだと視認性が悪いため、デバイスを操作してredへ変更した。しかし、すぐにまたgreenに戻ってしまう。」
この理由を説明します。現場での設定変更と同時にデバイス設定に合わせて reported は同期的に更新されます。この場合にも desired と reported の不一致が発生します。すると、以下のようなdelta が発生します。
{
"version": 8,
"timestamp": 1777347718,
"state": {
"color": "green"
},
"metadata": {
"led": {
"timestamp": 1777345874
},
"color": {
"timestamp": 1777347718
}
}
}Device Shadow での仕様として、desired こそあるべき姿であるためこちらを正として差分(delta)が出力されます。今回の場合はLEDカラーは red の方が正しい設定のはずなのに指示としては「color を green にせよ」というものになってしまいます。
つまり、そのまま reported だけを書き換えると、desired が古い値のまま残り、再接続のたびに delta が発生して意図しない上書きが起こってしまいます。
「現場での設定変更を許容せず、クラウド管理者へ申請し、クラウドからウォータフォールで設定変更するワークフローを徹底すれば良い。」というご意見もあるかと思います。要件によってはそれでも問題ないかもしれませんが、管理するデバイス数や設定管理の目的・要件次第では、クラウドはリモートで設定状況の把握と変更ができればよく、現場での変更も柔軟に取り入れる必要があったり、ウォーターフォールを維持するために運用効率が著しく落ちてしまうパターンもあり得ます。
落とし穴の回避策 desired をなくす
この問題の解決のヒントはブローカーが delta を計算する際のある仕様にあります。 delta は desired と reported に差分がある場合に検出されますが、 desired が存在しない場合にはそのチェックを実施しません。
そのため、次のフローの実施で現場で起きたことを正として扱うことができます。
① desired を更新してデバイス指示を伝達する
② 受け取ったデバイスで反映する
③ reported の更新とともに指示の元となった desired を null で消し込み
結論としては desired が存在しない状態を基本とする運用にすることで reported 主導の更新フローが実現できます。この前提で先ほどまでと同じことをしようとすると以下のようになります。
まず、Shadow ドキュメントは reported のみ存在します。
{
"state": {
"reported": {
"led": "off",
"color": "green",
"interval": 30
}
}
}1. クラウドからLEDを on へ変更指示
クラウドから指示を出す場合のみ一時的に desired が作成されます。
{
"state": {
"desired": {
"led": "on"
},
"reported": {
"led": "off",
"interval": 30,
"color": "red"
}
}
}するとブローカーがそれを検知して delta トピックに以下のような差分がパブリッシュされます。もちろん color の変更は差分として検知されません。
{
"version": 14,
"timestamp": 1777351497,
"state": {
"led": "on"
},
"metadata": {
"led": {
"timestamp": 1777351497
}
}
}デバイス側は delta での指示を元に設定を変更し、 reported を更新します。その際に desired の削除も同時に行います。その結果以下のようにShadow ドキュメントが更新されます。
{
"state": {
"reported": {
"led": "on",
"interval": 30,
"color": "red"
}
}
}フロー図に起こすと以下のようなイメージです。

2. 現場でLEDカラーを red へ変更
単純に reported が更新されるだけなので delta は発生せずクラウドでは受け入れられます。
{
"state": {
"reported": {
"led": "off",
"color": "red",
"interval": 30
}
}
}フロー図に起こすと以下のようなイメージです。

この設計で運用を構築することでクラウドからの指示もデバイスでの設定変更もどちらも柔軟に対応することができます。
なぜShadowの削除や空文字ではなく、nullなのか
Device Shadow の「状態を消す」操作には、見た目が似ていても挙動が大きく異なる 3 種類があります。
| 操作 | ペイロード例 | 効果 |
|---|---|---|
delete トピックへ publish | (ペイロードなし、または {}) | Shadow ドキュメント自体を削除。 |
desired を null に | {"state":{"desired":null}} | 該当セクションのキーを削除。version は加算 |
{} を送信 | {"state":{"desired":{}}} | 「desired は空オブジェクト」という意味になり、ドキュメントは残る |
特に注意したいのは、null と {} が 意味的に別物 であることです。null は「キーごと消す」、{} は「キーは存在するが中身は空」という宣言になります。{} のままだと、後続の差分計算や監視ロジックが「desired は明示的に空」を意図したものと解釈してしまうため、消し込みを目的とする場合は必ず null を使う必要があります。
delete トピックは、Thing の廃棄や検証用環境のクリーンアップなど、ドキュメント全体を初期化したい場面でだけ使うようなもので、基本的には本番運用中の状態反映には使用しません。
なぜこうまでしてDevice Shadowを使いたいのか
冒頭でも説明した通り、Device Shadow の大きな利点として、デバイスがオフラインでもクラウド側で最新の desired を保持できる点です。再接続時にデバイスが $aws/things/<thing>/shadow/get をリクエストすれば、そのときの差分を delta として受け取り、安全に追従できます。
これをIoT Core の別機能やその他のAWSサービスで実装しようとする場合どのようになるかまとめてみました。
| 手法 | 双方向同期 | 差分通知 | 競合制御 | 履歴保持 | オフライン耐性 |
|---|---|---|---|---|---|
| Device Shadow | 得意 | あり | あり | なし | 強い |
| MQTT Retained Message | 一方向の最後の値のみ | なし | なし | なし | 受信側のセッション次第 |
| DynamoDB ベースの自前管理 | 自前実装 | 自前実装 | 条件付き書き込みで可 | 任意で可能 | 自前実装 |
Retained Message は実装は手軽ですが、「希望状態と実状態の同居」「差分通知」のような Shadow 特有の機能は得られません。DynamoDB などのDBサービスを経由する場合はあらゆることを自前で実装する必要があります。そのため「IoT 由来の状態同期」が要件であれば、Device Shadowの使用が第一候補になるのです。
まとめ
本記事でお伝えしたのは、単なるAWSの機能紹介ではなく私自身が実際に壁にぶつかりながら得た、クラウドと現場の実運用を両立させるためのリアルなIoT運用ノウハウです。
当初は「reportedを最新化するだけで乗り切れるだろう」と考えていました。しかし、ドキュメントを読んだだけの基本的な使い方だけではクラウドからの一方向の変更しかできず、現場からの reported 先行による双方向的な設定変更が実現できないという壁に突き当たりました。 その後、desired を消すことで解決できるとわかった後も、null と {} の仕様を勘違いし、空のオブジェクトを送っていつまでも差分が出続けるという間違った実装をしてしまっていました。
IoTシステムの開発は、ドキュメントには書かれていない「現場の落とし穴」との戦いです。 desired / reported / delta の役割分担を正しく理解し、差分同期とオフライン復帰までを仕組みとして織り込んでこそ、初めて実運用に耐えうる同期基盤になります。私が遠回りして得たこの知見が、これからIoTシステムを構築する皆さんの試行錯誤を少しでもショートカットするヒントになれば嬉しいです。
投稿者プロフィール

-
2021年入社。BS事業部所属。
入社当初から一貫してAWSを用いた開発案件に携わっており、現在はサーバレス構成を基本としたプロジェクトに従事しています。
クラウドは進歩の早い技術領域ですので、最新技術のキャッチアップを日々積極的に行っています。

