AWS IoT Greengrass デバイス上に開発したカスタムコンポーネントで IoT シャドウを操作する機会があったのですが、設定すべきものが各所に散らばっており、ドキュメントもそれぞれに分かれていてハマりどころが多いため、自分の整理のためにまとめました。
全体像と設定すべきものまとめ
Greengrass デバイス上のカスタムコンポーネントから IoT シャドウを操作するために、以下の設定を行います。
- デバイスに紐付く IoT ポリシーステートメントで、 DeviceShadow ポリシーアクションとして対象リソースのシャドウへの操作を許可する
- aws.greengrass.ShadowManager コンポーネントをデプロイする。その際、デプロイ設定内の
synchronize
フィールドでローカルシャドウへの同期対象とするシャドウを指定する- 2'. カスタムコンポーネントから操作可能なシャドウを厳密に制限するため、コンポーネントのデプロイ設定内の "interpolateComponentConfiguration" フィールドを true に設定し、レシピ設定にレシピ変数を利用可能にする
- シャドウを取得したいコンポーネントに、 ShadowManager へのオペレーション許可を付与する
- 方法1: コンポーネントのレシピ内で、
ComponentConfiguration.DefaultConfiguration.accessControl
フィールドで指定 - 方法2: コンポーネントをデプロイする際の
accessControl
フィールドで指定
- 方法1: コンポーネントのレシピ内で、
- コンポーネント内のアプリケーションで、 AWS IoT Device SDK を用いてシャドウの操作を行う
詳細
以下、設定すべきものの概要を示しつつ、実際の設定手順を折りたたみのセクションで説明します。手順の部分については、AWS IoT Greengrass V2 入門ハンズオン の「3. Greengrass のセットアップ」 までが完了した環境を例として記載していきます。
0. デバイスのシャドウを作成する
AWS IoT のシャドウには、
- クラシックシャドウ: モノごとに 1 つだけ持てる、固有の名前を持たないシャドウ
- 名前付きシャドウ: シャドウ名でアクセスできるシャドウ、デバイス内で複数の名前付きシャドウを持てる
の2つがありますが、今回は両方を設定します。それぞれのシャドウの JSON として、以下の JSON を設定することにします。
{
"status":"alive"
}
{
"tempreture": 25
}
詳細な設定手順
まず、 AWS IoT のコンソールから、モノの管理画面で当該デバイスの管理画面に移動します。
Device Shadow タブに移動し、Device Shadow を作成します。
まずは、名前のないクラシックシャドウを作成します。
作成後の画面で再び Device Shadow の作成を開き(スクショ省略)、condition
という名前の名前付きシャドウを作成します。
次に、それぞれのシャドウに JSON の値をセットしていきます。クラシックシャドウをクリックしてシャドウの画面に移動し、
編集ボタンで編集モーダルウィンドウを表示したら、
シャドウの値を書き換えます。
今回は以下の値で書き換えました。なお、既存のキーの値に null
を設定することでキーごと削除することができるため、AWS IoT がデフォルトで作成している welcome
キーをこの方法で削除しています。
{
"state": {
"desired": {
"status": "alive",
"welcome": null
}
}
}
名前付きシャドウ (condition
) も同様に設定していきます。
{
"state": {
"desired": {
"tempreture": 25,
"welcome": null
}
}
}
1. デバイスに紐付く IoT ポリシーステートメントで、 DeviceShadow ポリシーアクションとして対象リソースのシャドウへの操作を許可する
Greengrass コアデバイスへの権限付与は、
- デバイスに紐付く IoT 証明書
- 証明書に紐付く IoT ポリシー
- (さらに、ポリシーにロールエイリアス経由で紐付く IAM ロール)
という関係性で行います。今回はデバイスシャドウを操作したいため、IoT ポリシーにデバイスシャドウへのアクセス権限を付与する必要があります。
※ 参考ドキュメント :
- AWS IoT Core ポリシー - AWS IoT Core
- ローカルデバイスシャドウを AWS IoT Core と同期する - AWS IoT Greengrass - 「前提条件」セクション
詳細な設定手順
AWS IoT のモノの管理画面で Greengrass デバイスを表示した状態で、「証明書」タブから証明書をクリックします。
証明書に紐付いたポリシーのうち、IoT シャドウの操作権限を付けたいポリシーをクリックします。 (今回は GreengrassV2IoTThingPolicy を選択)
ポリシーの編集画面で、「アクティブなバージョンを編集」をクリックします。
JSON としての編集に遷移し、 ポリシードキュメントの Statement
配下に以下を追加し、編集したバージョンをアクティブにしたうえで保存します。
※ <region>
, <account-id>
は自身の環境に合わせて変更する
{
"Effect": "Allow",
"Action": [
"iot:UpdateThingShadow",
"iot:GetThingShadow",
"iot:DeleteThingShadow"
],
"Resource": "arn:aws:iot:<region>:<account-id>:thing/*"
},
なお、本当は許可対象のシャドウを自身のデバイスシャドウに限定するために、 ${iot:Connection.Thing.ThingName}
というポリシー変数を利用したいところなのですが、Greengrass コアデバイスでは iot:Connection.Thing.*
ポリシー変数を利用できないという制約があります。そのため、実際に利用するデバイス名を考慮してワイルドカードでモノの名前を指定する必要があります。 (ここのドキュメントに気付かずずっとハマっていた……)
モノのポリシー変数 (iot:Connection.Thing.) は コアデバイスまたは Greengrass データプレーン操作用の AWS IoT ポリシーではサポートされていません。代わりに、ワイルドカードを使用して名前が似ている複数のデバイスと一致させることができます。たとえば、MyGreengrassDevice と指定すると MyGreengrassDevice1、MyGreengrassDevice2 などと一致します。
今回は汎用的な手順にするため、デバイス名全体をワイルドカードとして指定しました。
参考ドキュメント:
2. aws.greengrass.ShadowManager コンポーネントの設定とデプロイ
Greengrass には、コアデバイスでローカルシャドウサービスを実行するためのコンポーネントとして aws.greengrass.ShadowManager コンポーネントが用意されています。
クラウド上に保持されているデバイスシャドウとの同期を行うには、デプロイ時に synchronize
フィールドで
- どのデバイスシャドウを同期するか
- どの方向で同期するか (双方向、片方向)
などを設定する必要があります。(デフォルトの設定では、どのデバイスシャドウも同期対象になっていないため、ローカルシャドウしか利用できない)
具体的には、以下のような設定をシャドウマネージャーのデプロイ時に追加します。 Greengrass コアデバイスのクラシックシャドウと condition
という名前付きシャドウを、リアルタイムで双方向に同期する設定となります。
{
"strategy": {
"type": "realTime"
},
"synchronize": {
"coreThing": {
"classic": true,
"namedShadows": [
"condition"
]
},
"direction": "betweenDeviceAndCloud"
}
}
※ 参考ドキュメント :
- ローカルデバイスシャドウを AWS IoT Core と同期する - AWS IoT Greengrass - 「シャドウマネージャーコンポーネントを設定する」セクション
- シャドウマネージャー - AWS IoT Greengrass - 「構成」セクション
さらに、aws.greengrass.Nucleus コンポーネントで、レシピ内でのレシピ変数利用の有効化設定を行う
後続の 3. で、カスタムコンポーネントに対してシャドウの操作権限を与えます。そこでの対象リソースを厳密に絞るために、デバイス名をレシピ変数としてレシピに渡すことができます。
レシピ変数をレシピ内で利用するためには、aws.greengrass.Nucleus コンポーネントの interpolateComponentConfiguration
フィールドを true
にする必要があります。デプロイ時の設定値として、以下の JSON を設定します。
{
"interpolateComponentConfiguration": true
}
詳細な設定手順
Greengrass のデプロイの一覧から、当該コアデバイスに対するデプロイメントを選択し、変更に進みます。
最初のページでは何も変更せず次に進みます。
コンポーネントの選択画面では、パブリックコンポーネントの一覧から aws.greengrass.ShadowManager と aws.greengrass.Nucleus を追加します。
次に、 aws.greengrass.ShadowManager のコンポーネント設定を変更します。
コンポーネント設定としては、以下の設定をマージします。Greengrass コアデバイスのクラシックシャドウと condition
という名前付きシャドウを、リアルタイムで双方向に同期する設定となります。
{
"strategy": {
"type": "realTime"
},
"synchronize": {
"coreThing": {
"classic": true,
"namedShadows": [
"condition"
]
},
"direction": "betweenDeviceAndCloud"
}
}
同様の手順で、 aws.greengrass.Nucleus についても設定を変更します。入力する JSON は以下の通りです。
{
"interpolateComponentConfiguration": true
}
戻った先の画面以降は「次へ」→「次へ」→「デプロイ」と進めて、デプロイを完了させます。 (スクリーンショット省略)
ここまでの設定がうまく行っているかを確認したい場合、デバイス上で /greengrass/v2/logs/greengrass.log
を確認するとよいでしょう。同ログを ShadowManager
で grep することで、クラウドのシャドウをデバイス側で取得できているかを確認することができます。私は、ターミナルを2ウィンドウ開き、それぞれ以下のコマンドを実行しておくことで、定期的に ShadowManager が再起動して再同期しようとする際に同期成否がわかるようにしていました。
# ウィンドウ1: ShadowManager ログ確認
sudo tail -f /greengrass/v2/logs/greengrass.log | grep ShadowManager
# ウィンドウ2: ShadowManager を定期的に再起動
while [ true ] ; do date ; sudo /greengrass/v2/bin/greengrass-cli component restart -n aws.greengrass.ShadowManager ; sleep 10 ; done
3. シャドウを取得したいコンポーネントに、 ShadowManager へのオペレーション許可を付与する
ShadowManager コンポーネントをデプロイしたことで、他のカスタムコンポーネントから同コンポーネントに IPC 通信することでデバイスシャドウを操作できるようになりました。ただ、カスタムコンポーネントから ShadowManager コンポーネントに対する通信の許可は明示的に行う必要があるため、この設定を行います。
方法としては、
- カスタムコンポーネントのレシピ内で、 ComponentConfiguration.DefaultConfiguration.accessControl フィールドで指定
- カスタムコンポーネントをデプロイする際の accessControl フィールドで指定
の2つの方法がありますが、デバイスシャドウの操作権限はデプロイ環境問わず必要になるものだと考えられるため、今回は 1. の方法で設定していきます。
今回の手順では、新規の com.example.HelloWorld
カスタムコンポーネントを作成し、そこにこの権限設定を加えていきます。
※ 参考ドキュメント :
- Use the AWS IoT Device SDK to communicate with the Greengrass nucleus, other components, and AWS IoT Core - AWS IoT Greengrass - 「Authorize components to perform IPC operations」セクション
- ローカルシャドウとやり取り - AWS IoT Greengrass - 「認証」セクション
詳細な設定手順
今回は、 Greengrass V2 ハンズオンの Cloud9 環境から作業を行います。
まず、コンポーネントの最低限のファイル・フォルダを作成します。ターミナルを開き、以下コマンドを実行します。
mkdir -p ~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0
touch ~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0/hello_world.py
touch ~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0/requirements.txt
mkdir -p ~/environment/GreengrassCore/recipes
touch ~/environment/GreengrassCore/recipes/com.example.HelloWorld-1.0.0.yaml
それぞれのファイルを以下のように編集します。なお、requirements.txt
は一旦空のままで大丈夫です。
import sys
import time
import os
import logging
import signal
# ロガー設定
logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logging.basicConfig()
logger.info("==================== [App Initiatlized] ====================")
INTERVAL = 10
def device_main():
logger.info("-- env --")
logger.info(os.environ)
device_name = os.environ['AWS_IOT_THING_NAME']
while True:
logger.info(f"App Running on {device_name}......")
time.sleep(INTERVAL)
def exit_handler(_signal, frame):
"""
Exit handler
"""
logger.info("==================== [App finished] ====================")
exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, exit_handler)
device_main()
こちらはまだ、シャドウを触る部分は一切コードに含めていません。
RecipeFormatVersion: 2020-01-25
ComponentName: com.example.HelloWorld
ComponentVersion: 1.0.0
ComponentDescription: Example app
ComponentPublisher: Example company
ComponentConfiguration:
DefaultConfiguration:
accessControl:
aws.greengrass.ShadowManager:
'com.example.com.example.HelloWorld:shadow:1':
policyDescription: 'Allows access to shadows'
operations:
- 'aws.greengrass#GetThingShadow'
- 'aws.greengrass#UpdateThingShadow'
- 'aws.greengrass#DeleteThingShadow'
resources:
- $aws/things/{iot:thingName}/shadow # クラシックシャドウ向けの許可
- $aws/things/{iot:thingName}/shadow/name/condition # condition という名前付きシャドウへの許可
'com.example.com.example.HelloWorld:shadow:2':
policyDescription: 'Allows access to things with shadows'
operations:
- 'aws.greengrass#ListNamedShadowsForThing'
resources:
- '{iot:thingName}'
Manifests:
- Platform:
os: linux
Lifecycle:
Install:
Script: |
python3 -m venv .
./bin/python3 -m pip install -r {artifacts:path}/requirements.txt
Run:
RequiresPrivilege: true
Script: |
./bin/python3 -u {artifacts:path}/hello_world.py
このレシピの yaml のポイントが、 accessControl
ディレクティブです。今回構築する com.example.HelloWorld
カスタムコンポーネントに、 aws.greengrass.ShadowManager
向けのアクションを許可しています。accessControl
ディレクティブの書き方は、こちらのドキュメントにそのまま使えるサンプルがあるのを流用しています。
なお、ここでは {iot:thingName}
というコンポーネント変数を対象リソース指定に含めることでセキュアに設定を行っています。この変数の展開を有効にするための設定が、前述の Nucleus コンポーネントに対する interpolateComponentConfiguration
設定でした。
参考ドキュメント:
ここまで完了したら、一旦シャドウ取得を抜きにしてコンポーネントをローカルデプロイし、まずは起動できるかを確認してみます。
# コンポーネントのローカルデプロイ
sudo /greengrass/v2/bin/greengrass-cli deployment create \
--recipeDir ~/environment/GreengrassCore/recipes \
--artifactDir ~/environment/GreengrassCore/artifacts \
--merge "com.example.HelloWorld=1.0.0"
# 実行状況の確認
sudo tail -F /greengrass/v2/logs/com.example.HelloWorld.log
デプロイが成功すれば、10秒おきに起動中である旨のログメッセージが流れてきます。この段階ではアプリからシャドウにアクセスはしていません。
4. コンポーネント内のアプリケーションで、 AWS IoT Device SDK を用いてシャドウの操作を行う
最後に、コンポーネントのアプリのコードの中にデバイスシャドウとのやりとりを追加していきます。参考ドキュメントに Java / Python / JS のサンプルコードがあり、これを参考に作成しています。また、 AWS IoT Device SDK を利用するため、 Install
フェーズで利用する requirements.txt に awsiotsdk
を追加してインストールしています。
※ 参考ドキュメント :
- ローカルシャドウとやり取り - AWS IoT Greengrass - 「GetThingShadow」セクション
詳細な設定手順
requirements.txt と hello_world.py を、それぞれ以下のように変更します。
awsiotsdk
import sys
import time
import os
import logging
import signal
import awsiot.greengrasscoreipc
# import awsiot.greengrasscoreipc.client as client
from awsiot.greengrasscoreipc.model import GetThingShadowRequest
# ロガー設定
logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logging.basicConfig()
logger.info("==================== [App Initiatlized] ====================")
TIMEOUT = 10
INTERVAL = 10
def device_main():
logger.info("-- env --")
logger.info(os.environ)
device_name = os.environ['AWS_IOT_THING_NAME']
# set up IPC client to connect to the IPC server
ipc_client = awsiot.greengrasscoreipc.connect()
# クラシックシャドウ向けの GetThingShadowRequest 作成
# shadow_name = "" とすることでクラシックシャドウを指定できる
get_thing_classic_shadow_request = GetThingShadowRequest()
get_thing_classic_shadow_request.thing_name = device_name
get_thing_classic_shadow_request.shadow_name = ""
# 名前付きシャドウ (condition) 向けの GetThingShadowRequest 作成
# shadow_name にシャドウ名を指定する
get_thing_condition_shadow_request = GetThingShadowRequest()
get_thing_condition_shadow_request.thing_name = device_name
get_thing_condition_shadow_request.shadow_name = "condition"
while True:
logger.info("Shadow Test Running......")
# クラシックシャドウの取得・出力
op = ipc_client.new_get_thing_shadow()
op.activate(get_thing_classic_shadow_request)
fut = op.get_response()
result = fut.result(TIMEOUT)
logger.info(f"Classic Shadow: {result.payload.decode()}")
# 名前付きシャドウ (condition) の取得・出力
op = ipc_client.new_get_thing_shadow()
op.activate(get_thing_condition_shadow_request)
fut = op.get_response()
result = fut.result(TIMEOUT)
logger.info(f"Named Shadow: {result.payload.decode()}")
time.sleep(INTERVAL)
def exit_handler(_signal, frame):
"""
Exit handler
"""
logger.info("-------fin-------")
exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, exit_handler)
device_main()
GetThingShadowRequest
には thingName
と shadowName
を指定しますが、この shadowName
を空文字列 (""
) にすればクラシックシャドウへのリクエストに、シャドウの名前にすれば名前付きシャドウへのリクエストになります。
これでうまく行くか確認してみましょう。
# コンポーネントのローカルデプロイ
sudo /greengrass/v2/bin/greengrass-cli deployment create \
--recipeDir ~/environment/GreengrassCore/recipes \
--artifactDir ~/environment/GreengrassCore/artifacts \
--merge "com.example.HelloWorld=1.0.0"
# コンポーネントの再起動
sudo /greengrass/v2/bin/greengrass-cli component restart -n com.example.HelloWorld
# 実行状況の確認
sudo tail -F /greengrass/v2/logs/com.example.HelloWorld.log
うまく行っていれば、ログ上に10秒おきにシャドウのドキュメントが表示されるはずです。
コンポーネントのパブリッシュとクラウドからのデプロイについては省略します。
おわりに
Greengrass デバイス上のカスタムコンポーネントから IoT シャドウを操作するための一連の設定を見てきました。設定箇所は多いものの、一度そこをクリアしてしまえばアプリからは非常にシンプルにデバイスシャドウを扱うことができて便利ですね。
おまけ
今回の手順の確立にあたっての副産物は以下です。
あとは、 greengrass cli に alias 設定しておくと便利ですね。
echo 'alias gcli="sudo /greengrass/v2/bin/greengrass-cli"' >> ~/.bash_profile
source ~/.bash_profile