【Greengrass V2】定期的にセンサーデータを取得する Lambda コンポーネントを AWS SAM で作ってみた

Greengrass V2 の Lambda コンポーネントをセンサーデバイスを使って試してみました。
2021.08.19

前回の記事では、カスタムコンポーネントで温湿度データを取得しましたが、今回はこれを Lambda として作成してみました。

全体の構成は下記のようになります。(デバイス間の結線は簡略化して描いています。)

01-lambda-component-deploy-diagram

Lambda 関数を作るのはどんな方法でも構いませんが、今回は AWS SAM を使うことにしました。Lambda による処理の内容は下記のとおりです。

  • 前回の記事にあるコードをベースに Modbus-RTU 経由で温湿度データを取得
  • 取得した温湿度データを所定のファイルに出力
    • /tmp/Greengrass_modbus_sensor_lambda.log
  • 10秒間隔でデータを取得してファイル出力

なお、デバイス側の設定は前回の記事の内容を踏襲するものとします。
ggc_user の所属グループの変更や pymodbusのインストール等です)

Raspberry Pi の Python バージョン

こちらも前回と変更はありません。

$ python -V
Python 2.7.16

$ python3 -V
Python 3.7.3

上記のとおりデフォルトのバージョンは 2.7.16 ですが、Lambda のランタイムバージョンに合わせて python3 をデフォルトにする必要は有りませんでした。動作確認の節も合わせてご確認ください)

Lambda 関数の作成

まずは Lambda 関数を作っていきます。AWS SAM に関する説明は割愛させていただきます。
作業は AWS SAM をインストールした Mac で行います。

sam init

Raspberry Pi 側の Python は 3.7.3 なのでランタイムも同じバージョンを指定しています。

$ sam init \
    --runtime python3.7 \
    --name modbus-sensor-lambda-component \
    --app-template hello-world \
    --package-type Zip

SAM テンプレート

Lambda コンポーネントではバージョン指定が必要なので、AutoPublishAliasを指定しています。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  modbus-sensor-lambda-component

  Sample SAM Template for modbus-sensor-lambda-component

Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      AutoPublishAlias: dev

Outputs:
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Lambdaコード

from pymodbus.client.sync import ModbusSerialClient as ModbusClient
import datetime
import time

def run_sync_client():
    client = ModbusClient(baudrate=9600, port="/dev/ttyUSB0", method="rtu")
    client.connect()
    rr = client.read_input_registers(address=1, count=2, unit=0x1)
    temperature = rr.registers[0]/10
    humidity  = rr.registers[1]/10

    # Append the message to the log file.
    with open('/tmp/Greengrass_modbus_sensor_lambda.log', 'a') as f:
        print(f"{str(datetime.datetime.now())}\tTemperatur:\t{temperature} ℃", file=f)
        print(f"{str(datetime.datetime.now())}\tHumidity:\t{humidity} %", file=f)

    client.close()

while True:
    run_sync_client()
    time.sleep(10)


def lambda_handler(event, context):
    return

この Lambda コードは Greengrass の設定で 「存続期間の長い Lambda (Long-Lived Lambda)」になっていることを想定したものになっています。

Long-Lived Lambda の場合、トピックに対するメッセージを受信しなくても、デプロイされた後に自動的に Lambda 関数が実行されます。ハンドラーの前にある初期化処理でループ処理を実装することで定期処理を行っています。

「存続期間の長い Lambda」について

Greengrass V2 の Lambda コンポーネントでは、デフォルトで 「存続期間の長い Lambda」 としてデプロイされます。「存続期間が長い」という表現は他にも「Long-Lived」や「Pinned」と表現されることがあります。
個人的には「Long-Lived Lambda」と表現するのが分かりやすいかなと思っています。

Long-Lived Lambda の詳細については下記の Blackbelt の資料が分かりやすいです。

確認した限りでは、Greengrass V1 と V2 で Long-Lived Lambda の仕様の違いは無いようです。合わせて下記も参考になるかと思います。

ビルド

利用しているラズパイ上の Python のバージョンは 3.7.3 なので、同じバージョンのビルドイメージを使ってビルドしました。
(Docker Desktop for Mac がインストール済みで起動している前提です)

$ sam build \
    --use-container \
    --build-image Function1=amazon/aws-sam-cli-build-image-python3.7

デプロイ

ビルドできたらデプロイします。アーティファクトが置かれる S3 バケットはご利用の任意のものを指定いただければと思います。

$ sam package \
    --output-template-file packaged.yaml \
    --s3-bucket xxxxxxxxxx
$ sam deploy \
    --template-file packaged.yaml \
    --stack-name modbus-sensor-lambda-component-stack \
    --s3-bucket xxxxxxxxxx \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

Lambda コンポーネントの作成

Lambda関数を作成できたら、Greengrass サービスからデバイスに Lambda 関数をデプロイします。
最初にコンポーネントを作成します。

02-make-component

次の画面で 「Lambda 関数 をインポートする」を選択します。
その下のプルダウンから先程 AWS SAM で作成した Lambda 関数を選択してください。AWS SAM でバージョンも付与されているので最新バージョンを選択します。(環境によりデプロイしたいバージョンを指定してください)

ちなみに、対象の Lambda のコンポーネント作成が初回の場合、コンポーネントバージョンを指定しなければ Lambda のバージョンがコンポーネントバージョンになります。
(例:Lambda バージョンが 5 なら、コンポーネントバージョンは 5.0.0

03-make-component-import-lambda

オプション設定は全てデフォルトのままとしました。下記の「固定済み」の指定では Lambda の実行方式を選択します。デフォルトで「Long-Lived」 が選択されているので変更せずにそのままとします。

04-pinned-defalt

次のオプション設定では、Lambda コンポーネントのコンテナ化の設定になります。デフォルトでは Greengrass コンテナ内で実行されますが「Greengrass コンテナ」を選択すると、アクセスを許可したいデバイスのパスやボリュームを指定してセキュアに Lambda コンポーネントを稼働させることができます。

今回は動作確認を優先したかったので「コンテナなし」 を選択しました。この場合は Lambda コンポーネントから任意のデバイスやボリュームにアクセスできます。
最後に「コンポーネントを作成」 をクリックします。

05-no-container-mode

Lambda コンポーネントのデプロイ

コンポーネントが作成できたら、作成したコンポーネントの画面に遷移するのでそのまま「デプロイ」をクリックしてデプロイ作業に進みましょう。

06-deploy-lambda-component

次の画面ではデプロイしたい対象を選択します。下記の記事のようにデバイスに Greengrass Core ソフトウェアをデプロイしていれば、そのデプロイが存在します。

07-add-deploy

次の画面ではそのまま次へ進みます。(名前を変えたりタグを付けたい場合は設定してください)

08-select-target

「コンポーネントの選択」画面では、デプロイしたいコンポーネントを選択します。デプロイしたい Lambda コンポーネントがデフォルトで選択されているので、そのまま次へ進みます。

09-select-component

次の画面は今回は特にやることが無いので、そのまま次に進みます。

10-config-component-option

Greengrass V2 のコンポーネントデプロイでは、AWS IoT のジョブ機能が使われています。ここではそのジョブの設定を行います。今回はデフォルトで進めました。

11-detail-config-option

最後にレビュー画面で設定を確認します。

12-review

レビューで問題なければ画面下にある「デプロイ」をクリックしてデプロイを開始します。

13-review-deploy

先程書いたように「ジョブ機能」を使ってデプロイされるので、ジョブの画面でデプロイ状況の詳細を見ることができます。

14-deploy-job

デプロイが成功しました。

15-succeed-deploy

動作確認

無事にデプロイできたか Raspberry Pi 上で確認します。

$ sudo /greengrass/v2/bin/greengrass-cli component list

コンポーネントの一覧から Lambda コンポーネントを確認できました。

(結果を一部抜粋)

Component Name: modbus-sensor-lambda-component--HelloWorldFunction-xxxxxxxxxxxx
    Version: 1.0.0
    State: RUNNING
    Configuration: {"containerMode":"NoContainer","containerParams":{"devices":{},"memorySize":16000.0,"mountROSysfs":false,"volumes":{}},"inputPayloadEncodingType":"json","lambdaExecutionParameters":{"EnvironmentVariables":{}},"maxIdleTimeInSeconds":60.0,"maxInstancesCount":100.0,"maxQueueSize":1000.0,"pinned":true,"pubsubTopics":{},"statusTimeoutInSeconds":60.0,"timeoutInSeconds":3.0}

Lambda のコード内で指定したファイルを確認すると、きちんと温湿度が記録されていました!

$ tail -f /tmp/Greengrass_modbus_sensor_lambda.log 

2021-08-18 20:30:47.752837	Temperatur:	27.9 ℃
2021-08-18 20:30:47.752924	Humidity:	56.4 %
2021-08-18 20:30:57.918288	Temperatur:	27.9 ℃
2021-08-18 20:30:57.918383	Humidity:	56.4 %
2021-08-18 20:31:07.966270	Temperatur:	27.9 ℃
2021-08-18 20:31:07.966368	Humidity:	56.4 %
2021-08-18 20:31:18.137136	Temperatur:	27.9 ℃
2021-08-18 20:31:18.137234	Humidity:	56.4 %
2021-08-18 20:31:28.185414	Temperatur:	27.9 ℃
2021-08-18 20:31:28.185510	Humidity:	56.4 %

プロセスを確認すると、Raspberry Pi 上のデフォルトの Python は 2.7.16ですが 下記のように /usr/bin/python3.7 が使われていました。

$ ps aux | grep modbus-sensor-lambda-component

ggc_user 10326  0.1  0.3  29952 14756 ?        Sl   16:14   0:00 /usr/bin/python3.7 -u /greengrass/v2/work/modbus-sensor-lambda-component--HelloWorldFunction-xxxxxxxxxxxx/work/worker/0/runtime/python/lambda_runtime.py --handler=app.lambda_handler

試しに Lambda のランタイムを python3.8 にして試したところ、コンポーネントのデプロイは正常終了しますが、Raspberry Pi に Python3.8 がインストールされていないので実際の処理は正常に動いていないようでした。

最後に

従来の Lambda による開発手法がそのまま使える点はメリットだと思いますが、V1 の時と同様に下記のような点に注意が必要となります。

  • 動作確認のために毎回デバイスへのデプロイが必要
  • Lambda の ライフサイクルを意識した実装が必要
  • Lambda のコンテナ設定のためにデバイス側の構成を把握しておく必要がある

一方で、上記の点は逆にメリットとなる側面もあるため、カスタムコンポーネントと Lambda コンポーネントのどちらがいいのか、という点は総合的な観点で判断する必要があると思いました。

ノウハウ溜まってきたら改めてまとめてみたいと思います。

以上です。