Lambdaを使わずStep FunctionsでNature Remo Cloud APIのデータを収集する

2021.10.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

IoT事業部のやまたつです!

今日はAWS Lambdaを使わずにAWS Step FunctionsからNature RemoのCloud APIを叩いて、お部屋の温度、湿度、照度をAmazon CloudWatch metricsに収集したいと思います!

概要

Nature Remoとは

Nature株式会社からリリースされている製品です。アプリやスマートスピーカーから家電を操作できるようになります。

Nature Remo Cloud APIとは

Nature Remoを操作したり、Nature Remoに内蔵されているセンサーから得られる情報を取得したりできるWeb APIです。
今回はこれを使って温度、湿度、照度の情報を取得してみようと思います。

アウトプット

こんな感じでお部屋の温度と湿度の時系列データが確認できるようになります!

Temperatureをタイポしています!?

AWS Step Functionsに何をさせるのか

  1. Parameter StoreからAPIのtokenを取ってくる
  2. Amazon API Gatewayを介してNature Remo Cloud APIからデータを取得する
  3. Amazon CloudWatch metricsにPutする。

AWS Lambdaでやれば良いんじゃないの?

AWS Step Functionsでやってみたかった。それ以上の回答を、僕は持ち合わせていないですね。

やっていく!

aws-cdkで書いていきます!v2を使います!  

Parameter StoreからAPIのtokenを取ってくる

予め、Nature Remo Cloud APIのtokenをParameter Storeに入れておく必要があります。
僕は /plantor/nature-remo-token という名前で入れました!

aws ssm put-parameter --name '/plantor/nature-remo-token' --value 'YOUR-TOKEN' --type 'String' --data-type 'text'

cdkはこんな感じ。

import {
  App,
  Stack,
  StackProps,
  aws_stepfunctions as sfn,
  aws_stepfunctions_tasks as tasks,
} from "aws-cdk-lib";

type Props = StackProps & {};

export class NatureRemo extends Stack {
  constructor(parent: App, id: string, props: Props) {
    super(parent, id, props);

    const taskToGetSecret = new tasks.CallAwsService(this, "GetSecretTask", {
      service: "ssm",
      action: "getParameter",
      parameters: { Name: "/plantor/nature-remo-token" },
      iamResources: ["*"],
      iamAction: "ssm:GetParameter",
      resultSelector: {
        "Token.$": "$.Parameter.Value",
      },
      resultPath: "$.SecretOutput",
    });

    new sfn.StateMachine(this, "MyStateMachine", {
      definition: taskToGetSecret,
    });
  }
}

new tasks.CallAwsService() でParameter Storeのデータを取得しています。これは今年の9月に発表されたAWS Step FunctionsのAWS SDK統合の機能を使うものです。

発表から一週間程度でaws-cdkに機能が追加されてます。活きが良い!

これによりSDKさえ対応していればAWS Step Functionsから扱うことができます。すごい!

注意⚠️

今回のようにAWS Step Functions内で秘匿情報を扱うのは、個人プロダクトだけにするのが良いと思われます。なぜならAWS Step FunctionsのStateに格納される情報はWebコンソールなどから確認することができてしまうからです。大人しくLambdaを書きましょう。

Amazon API Gatewayを介してNature Remo Cloud APIからデータを取得する

次にNature Remo Cloud APIを叩けるようにしていきます!
まずはAmazon API Gatewayを用意します。

/**
 * AWS Step Functions の Amazon API Gateway Integration は Authorization header が使えない。
 * そのため、カスタムヘッダーに入れて、Amazon API Gateway側で request parameter mappingしてあげるワークアラウンドを実装している。
 * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html#connect-api-gateway-requests
 */
const xAuthorization = "method.request.header.x-Authorization";

const remoEndpoint = new aws_apigateway.RestApi(
  this,
  "NatureRemoEndpoint",
  {
    defaultIntegration: new aws_apigateway.HttpIntegration(
      "https://api.nature.global/1/devices",
      {
        options: {
          requestParameters: {
            "integration.request.header.Authorization": xAuthorization,
          },
        },
      },
    ),
  },
);
remoEndpoint.root.addMethod("GET", undefined, {
  requestParameters: { [xAuthorization]: true },
});

そしてAWS Step Functionsにて使います!

const taskToCallApi = new tasks.CallApiGatewayRestApiEndpoint(
  this,
  "CallNatureRemoTask",
  {
    api: remoEndpoint,
    stageName: remoEndpoint.deploymentStage.stageName,
    method: tasks.HttpMethod.GET,
    headers: sfn.TaskInput.fromObject({
      "x-Authorization": sfn.JsonPath.stringAt(
        /**
         * ドキュメントでは「Listでもいいよ」みたいに書いてあるけど、実際には配列じゃないと実行時エラーになる。
         * なので `States.Array()` が必要。
         * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
         */
        "States.Array(States.Format('Bearer {}', $.SecretOutput.Token))",
      ),
    }),
    resultSelector: {
      "Events.$": "$.ResponseBody[1].newest_events",
    },
    resultPath: "$.NatureRemoOutput",
  },
);

new sfn.StateMachine(this, "MyStateMachine", {
  definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric),
});

注意⚠️

コード中のコメントにもある通り、AWS Step FunctionsのAmazon API Gateway統合ではAuthorization headerを使うことは許可されていません。そのため上記コードではAuthorization headerを使わないワークアラウンドを実装しています。ご利用は自己責任でお願いします??‍♂️

Amazon CloudWatch metricsにPutする。

もう一息!もう一度、AWS Step FunctionsのSDK統合を使ってcloudwatch putMetricDataを呼び出します!

const taskToPutMetric = new tasks.CallAwsService(this, "PutMetricTask", {
  service: "cloudwatch",
  action: "putMetricData",
  parameters: {
    Namespace: "CUSTOM-IoT/Room",
    MetricData: [
      {
        MetricName: "Temperature",
        Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.te.val"),
      },
      {
        MetricName: "Illuminance",
        Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.il.val"),
      },
      {
        MetricName: "Humidity",
        Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.hu.val"),
      },
    ],
  },
  iamResources: ["*"],
  iamAction: "cloudwatch:PutMetricData",
  resultPath: "$.PutMetricOutput",
});

const stateMachine = new sfn.StateMachine(this, "MyStateMachine", {
  definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric),
});

完成!

コードの全体は以下のとおりです。

import {
  App,
  Duration,
  Stack,
  StackProps,
  aws_events,
  aws_events_targets,
  aws_stepfunctions as sfn,
  aws_stepfunctions_tasks as tasks,
  aws_apigateway,
} from "aws-cdk-lib";

type Props = StackProps & {};

export class NatureRemo extends Stack {
  constructor(parent: App, id: string, props: Props) {
    super(parent, id, props);

    const remoEndpoint = this.natureRemoEndpoint();

    const taskToGetSecret = new tasks.CallAwsService(this, "GetSecretTask", {
      service: "ssm",
      action: "getParameter",
      parameters: { Name: "/plantor/nature-remo-token" },
      iamResources: ["*"],
      iamAction: "ssm:GetParameter",
      resultSelector: {
        "Token.$": "$.Parameter.Value",
      },
      resultPath: "$.SecretOutput",
    });

    const taskToCallApi = new tasks.CallApiGatewayRestApiEndpoint(
      this,
      "CallNatureRemoTask",
      {
        api: remoEndpoint,
        stageName: remoEndpoint.deploymentStage.stageName,
        method: tasks.HttpMethod.GET,
        headers: sfn.TaskInput.fromObject({
          "x-Authorization": sfn.JsonPath.stringAt(
            /**
             * ドキュメントでは「Listでもいいよ」みたいに書いてあるけど、実際には配列じゃないと実行時エラーになる。
             * なので `States.Array()` が必要。
             * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
             */
            "States.Array(States.Format('Bearer {}', $.SecretOutput.Token))",
          ),
        }),
        resultSelector: {
          "Events.$": "$.ResponseBody[1].newest_events",
        },
        resultPath: "$.NatureRemoOutput",
      },
    );

    const taskToPutMetric = new tasks.CallAwsService(this, "PutMetricTask", {
      service: "cloudwatch",
      action: "putMetricData",
      parameters: {
        Namespace: "CUSTOM-IoT/Room",
        MetricData: [
          {
            MetricName: "Temperature",
            Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.te.val"),
          },
          {
            MetricName: "Illuminance",
            Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.il.val"),
          },
          {
            MetricName: "Humidity",
            Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.hu.val"),
          },
        ],
      },
      iamResources: ["*"],
      iamAction: "cloudwatch:PutMetricData",
      resultPath: "$.PutMetricOutput",
    });

    const stateMachine = new sfn.StateMachine(this, "MyStateMachine", {
      definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric),
    });

    new aws_events.Rule(this, "ScheduleRule", {
      schedule: aws_events.Schedule.rate(Duration.minutes(60)),
      targets: [new aws_events_targets.SfnStateMachine(stateMachine)],
    });
  }

  /**
   * 後ろ側にnatureRemoのAPIを設定したAmazon API Gateway
   */
  private natureRemoEndpoint() {
    /**
     * AWS Step Functions の Amazon API Gateway Integration は Authorization header が使えない。
     * そのため、カスタムヘッダーに入れて、Amazon API Gateway側で request parameter mappingしてあげるワークアラウンドを実装している。
     * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html#connect-api-gateway-requests
     */
    const xAuthorization = "method.request.header.x-Authorization";

    const remoEndpoint = new aws_apigateway.RestApi(
      this,
      "NatureRemoEndpoint",
      {
        defaultIntegration: new aws_apigateway.HttpIntegration(
          "https://api.nature.global/1/devices",
          {
            options: {
              requestParameters: {
                "integration.request.header.Authorization": xAuthorization,
              },
            },
          },
        ),
      },
    );
    remoEndpoint.root.addMethod("GET", undefined, {
      requestParameters: { [xAuthorization]: true },
    });

    return remoEndpoint;
  }
}

今回作成したコードはGitHubにもコミットしてあります。

まとめ

AWS Lambdaのコードを書かずに外部APIの結果をメトリクスデータとして可視化できました!
「結局cdkのためにtypescript書いてるやないかい」というツッコミは甘んじて受けます!

AWS Lambdaのコードを書くかどうかは置いておいても、SDK統合によってAWS Step Functionsが格段にパワーアップしたのを感じました!
もし機会があればぜひ使ってみてください!

以上、やまたつでした!