0
0

AWS IoT Greengrass v2 のカスタムコンポーネントから IoT シャドウを操作する

Last updated at Posted at 2024-07-27

AWS IoT Greengrass デバイス上に開発したカスタムコンポーネントで IoT シャドウを操作する機会があったのですが、設定すべきものが各所に散らばっており、ドキュメントもそれぞれに分かれていてハマりどころが多いため、自分の整理のためにまとめました。

全体像と設定すべきものまとめ

図1.png

Greengrass デバイス上のカスタムコンポーネントから IoT シャドウを操作するために、以下の設定を行います。

  1. デバイスに紐付く IoT ポリシーステートメントで、 DeviceShadow ポリシーアクションとして対象リソースのシャドウへの操作を許可する
  2. aws.greengrass.ShadowManager コンポーネントをデプロイする。その際、デプロイ設定内の synchronize フィールドでローカルシャドウへの同期対象とするシャドウを指定する
    • 2'. カスタムコンポーネントから操作可能なシャドウを厳密に制限するため、コンポーネントのデプロイ設定内の "interpolateComponentConfiguration" フィールドを true に設定し、レシピ設定にレシピ変数を利用可能にする
  3. シャドウを取得したいコンポーネントに、 ShadowManager へのオペレーション許可を付与する
    • 方法1: コンポーネントのレシピ内で、 ComponentConfiguration.DefaultConfiguration.accessControl フィールドで指定
    • 方法2: コンポーネントをデプロイする際の accessControl フィールドで指定
  4. コンポーネント内のアプリケーションで、 AWS IoT Device SDK を用いてシャドウの操作を行う

詳細

以下、設定すべきものの概要を示しつつ、実際の設定手順を折りたたみのセクションで説明します。手順の部分については、AWS IoT Greengrass V2 入門ハンズオン の「3. Greengrass のセットアップ」 までが完了した環境を例として記載していきます。

0. デバイスのシャドウを作成する

AWS IoT のシャドウには、

  • クラシックシャドウ: モノごとに 1 つだけ持てる、固有の名前を持たないシャドウ
  • 名前付きシャドウ: シャドウ名でアクセスできるシャドウ、デバイス内で複数の名前付きシャドウを持てる

の2つがありますが、今回は両方を設定します。それぞれのシャドウの JSON として、以下の JSON を設定することにします。

クラシックシャドウ
{
  "status":"alive"
}
名前付きシャドウ (名前: condition)
{
  "tempreture": 25
}
詳細な設定手順

まず、 AWS IoT のコンソールから、モノの管理画面で当該デバイスの管理画面に移動します。

image.png

Device Shadow タブに移動し、Device Shadow を作成します。

image.png

まずは、名前のないクラシックシャドウを作成します。

image.png

作成後の画面で再び Device Shadow の作成を開き(スクショ省略)、condition という名前の名前付きシャドウを作成します。

image.png

次に、それぞれのシャドウに JSON の値をセットしていきます。クラシックシャドウをクリックしてシャドウの画面に移動し、

image.png

編集ボタンで編集モーダルウィンドウを表示したら、

image.png

シャドウの値を書き換えます。

image.png

今回は以下の値で書き換えました。なお、既存のキーの値に null を設定することでキーごと削除することができるため、AWS IoT がデフォルトで作成している welcome キーをこの方法で削除しています。

{
  "state": {
    "desired": {
      "status": "alive",
      "welcome": null
    }
  }
}

名前付きシャドウ (condition) も同様に設定していきます。

{
  "state": {
    "desired": {
      "tempreture": 25,
      "welcome": null
    }
  }
}

1. デバイスに紐付く IoT ポリシーステートメントで、 DeviceShadow ポリシーアクションとして対象リソースのシャドウへの操作を許可する

図2.png

Greengrass コアデバイスへの権限付与は、

  • デバイスに紐付く IoT 証明書
  • 証明書に紐付く IoT ポリシー
  • (さらに、ポリシーにロールエイリアス経由で紐付く IAM ロール)

という関係性で行います。今回はデバイスシャドウを操作したいため、IoT ポリシーにデバイスシャドウへのアクセス権限を付与する必要があります。

※ 参考ドキュメント :

詳細な設定手順

AWS IoT のモノの管理画面で Greengrass デバイスを表示した状態で、「証明書」タブから証明書をクリックします。

image.png

証明書に紐付いたポリシーのうち、IoT シャドウの操作権限を付けたいポリシーをクリックします。 (今回は GreengrassV2IoTThingPolicy を選択)

image.png

ポリシーの編集画面で、「アクティブなバージョンを編集」をクリックします。

image.png

JSON としての編集に遷移し、 ポリシードキュメントの Statement 配下に以下を追加し、編集したバージョンをアクティブにしたうえで保存します。

<region>, <account-id> は自身の環境に合わせて変更する

    {
      "Effect": "Allow",
      "Action": [
        "iot:UpdateThingShadow",
        "iot:GetThingShadow",
        "iot:DeleteThingShadow"
      ],
      "Resource": "arn:aws:iot:<region>:<account-id>:thing/*"
    },

image.png

なお、本当は許可対象のシャドウを自身のデバイスシャドウに限定するために、 ${iot:Connection.Thing.ThingName} というポリシー変数を利用したいところなのですが、Greengrass コアデバイスでは iot:Connection.Thing.* ポリシー変数を利用できないという制約があります。そのため、実際に利用するデバイス名を考慮してワイルドカードでモノの名前を指定する必要があります。 (ここのドキュメントに気付かずずっとハマっていた……)

モノのポリシー変数 (iot:Connection.Thing.) は コアデバイスまたは Greengrass データプレーン操作用の AWS IoT ポリシーではサポートされていません。代わりに、ワイルドカードを使用して名前が似ている複数のデバイスと一致させることができます。たとえば、MyGreengrassDevice と指定すると MyGreengrassDevice1、MyGreengrassDevice2 などと一致します。

今回は汎用的な手順にするため、デバイス名全体をワイルドカードとして指定しました。

参考ドキュメント:

2. aws.greengrass.ShadowManager コンポーネントの設定とデプロイ

図3.png

Greengrass には、コアデバイスでローカルシャドウサービスを実行するためのコンポーネントとして aws.greengrass.ShadowManager コンポーネントが用意されています。

クラウド上に保持されているデバイスシャドウとの同期を行うには、デプロイ時に synchronize フィールドで

  • どのデバイスシャドウを同期するか
  • どの方向で同期するか (双方向、片方向)

などを設定する必要があります。(デフォルトの設定では、どのデバイスシャドウも同期対象になっていないため、ローカルシャドウしか利用できない)

具体的には、以下のような設定をシャドウマネージャーのデプロイ時に追加します。 Greengrass コアデバイスのクラシックシャドウと condition という名前付きシャドウを、リアルタイムで双方向に同期する設定となります。

{
  "strategy": {
    "type": "realTime"
  },
  "synchronize": {
    "coreThing": {
      "classic": true,
      "namedShadows": [
        "condition"
      ]
    },
    "direction": "betweenDeviceAndCloud"
  }
}

※ 参考ドキュメント :

さらに、aws.greengrass.Nucleus コンポーネントで、レシピ内でのレシピ変数利用の有効化設定を行う

後続の 3. で、カスタムコンポーネントに対してシャドウの操作権限を与えます。そこでの対象リソースを厳密に絞るために、デバイス名をレシピ変数としてレシピに渡すことができます。

レシピ変数をレシピ内で利用するためには、aws.greengrass.Nucleus コンポーネントの interpolateComponentConfiguration フィールドを true にする必要があります。デプロイ時の設定値として、以下の JSON を設定します。

{
  "interpolateComponentConfiguration": true
}
詳細な設定手順

Greengrass のデプロイの一覧から、当該コアデバイスに対するデプロイメントを選択し、変更に進みます。

image.png

最初のページでは何も変更せず次に進みます。

image.png

コンポーネントの選択画面では、パブリックコンポーネントの一覧から aws.greengrass.ShadowManager と aws.greengrass.Nucleus を追加します。

image.png

次に、 aws.greengrass.ShadowManager のコンポーネント設定を変更します。

image.png

コンポーネント設定としては、以下の設定をマージします。Greengrass コアデバイスのクラシックシャドウと condition という名前付きシャドウを、リアルタイムで双方向に同期する設定となります。

{
  "strategy": {
    "type": "realTime"
  },
  "synchronize": {
    "coreThing": {
      "classic": true,
      "namedShadows": [
        "condition"
      ]
    },
    "direction": "betweenDeviceAndCloud"
  }
}

image.png

同様の手順で、 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 へのオペレーション許可を付与する

図4.png

ShadowManager コンポーネントをデプロイしたことで、他のカスタムコンポーネントから同コンポーネントに IPC 通信することでデバイスシャドウを操作できるようになりました。ただ、カスタムコンポーネントから ShadowManager コンポーネントに対する通信の許可は明示的に行う必要があるため、この設定を行います。

方法としては、

  1. カスタムコンポーネントのレシピ内で、 ComponentConfiguration.DefaultConfiguration.accessControl フィールドで指定
  2. カスタムコンポーネントをデプロイする際の accessControl フィールドで指定

の2つの方法がありますが、デバイスシャドウの操作権限はデプロイ環境問わず必要になるものだと考えられるため、今回は 1. の方法で設定していきます。

今回の手順では、新規の com.example.HelloWorld カスタムコンポーネントを作成し、そこにこの権限設定を加えていきます。

※ 参考ドキュメント :

詳細な設定手順

今回は、 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 は一旦空のままで大丈夫です。

~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0/hello_world.py
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()

こちらはまだ、シャドウを触る部分は一切コードに含めていません。

~/environment/GreengrassCore/recipes/com.example.HelloWorld-1.0.0.yaml
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 を用いてシャドウの操作を行う

図4.png

最後に、コンポーネントのアプリのコードの中にデバイスシャドウとのやりとりを追加していきます。参考ドキュメントに Java / Python / JS のサンプルコードがあり、これを参考に作成しています。また、 AWS IoT Device SDK を利用するため、 Install フェーズで利用する requirements.txt に awsiotsdk を追加してインストールしています。

※ 参考ドキュメント :

詳細な設定手順

requirements.txt と hello_world.py を、それぞれ以下のように変更します。

~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0/requirements.txt
awsiotsdk
~/environment/GreengrassCore/artifacts/com.example.HelloWorld/1.0.0/hello_world.py
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 には thingNameshadowName を指定しますが、この 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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0