この記事は公開されてから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に何をさせるのか
- Parameter StoreからAPIのtokenを取ってくる
- Amazon API Gatewayを介してNature Remo Cloud APIからデータを取得する
- 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が格段にパワーアップしたのを感じました!
もし機会があればぜひ使ってみてください!
以上、やまたつでした!