AWS CDK v2.88.0 で、IoT rule actions の L2 Construct で HTTPS Actions がサポートされました

2023.07.24

こんにちは、CX事業本部 Delivery部の若槻です。

AWS IoT rule actions を使用すると、指定したトピックにデータがパブリッシュされた際に、Lambda 関数の呼び出しや Amazon Kinesis Data Streams へのデータの送信などのアクションを定義することができます。

そして先日リリースされた AWS CDK v2.88.0 で、IoT rule actions の L2 Construct で HTTPS Actions がサポートされました。

このアップデートにより、トピックにデータがパブリッシュされた際に、指定した HTTP エンドポイントへのメッセージのルーティングを容易に実装できるようになりました。

試してみる

必要なモジュールのインストール

AWS CDK のモジュールをアップグレードし、AWS Scheduler の Alpha モジュールをインストールします。

npm i aws-cdk aws-cdk-lib
npm i -D @aws-cdk/aws-iot-actions-alpha

CDK コード

README によると実装方法は下記のようになります。addActionHttpsAction を指定できるようになっています。

packages/@aws-cdk/aws-iot-actions-alpha/README.md

const topicRule = new iot.TopicRule(this, 'TopicRule', {
    sql: iot.IotSql.fromStringAsVer20160323(
      "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'",
    ),
  });

  topicRule.addAction(
    new actions.HttpsAction('https://example.com/endpoint', {
      confirmationUrl: 'https://example.com',
      headers: [
        { key: 'key0', value: 'value0' },
        { key: 'key1', value: 'value1' },
      ],
      auth: { serviceName: 'serviceName', signingRegion: 'us-east-1' },
    }),
  );
}

今回は認証が必要ないエンドポイントで試してみます。下記は検証用の CDK コードです。IoT Topic rule と合わせて、メッセージ送信先のエンドポイントとなる REST API を合わせて作成しています。

lib/cdk-sample-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import * as aws_lambda from 'aws-cdk-lib/aws-lambda';
import * as aws_apigateway from 'aws-cdk-lib/aws-apigateway';
import * as aws_iot_alpha from '@aws-cdk/aws-iot-alpha';
import * as aws_iot_actions_alpha from '@aws-cdk/aws-iot-actions-alpha';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const myFunction = new aws_lambda.Function(this, 'myFunction', {
      functionName: 'myFunction',
      handler: 'index.handler',
      runtime: aws_lambda.Runtime.NODEJS_18_X,
      code: new aws_lambda.InlineCode(`
      exports.handler = async (event) => {
        console.log(event);
        try {
          return {
            "statusCode": 200,
            "headers": {
              "Content-Type": "application/json"
            },
            "body": JSON.stringify({
              "message": "Hello, World!"
            })
          };
        } catch (error) {
          console.error(error);
          return {
            statusCode: 500,
            headers: {
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              error: "Internal Server Error"
            })
          };
        }
      };
      `),
    });

    // REST API
    const booksApi = new aws_apigateway.RestApi(this, 'booksApi', {});
    booksApi.root
      .addResource('books')
      .addMethod('POST', new aws_apigateway.LambdaIntegration(myFunction));

    // IoT topic rule
    const myTopicRule = new aws_iot_alpha.TopicRule(this, 'myTopicRule', {
      topicRuleName: 'myTopicRule',
      sql: aws_iot_alpha.IotSql.fromStringAsVer20160323(
        "SELECT * FROM 'topic/subtopic'"
      ),
      errorAction: new aws_iot_actions_alpha.LambdaFunctionAction(myFunction),
    });

    // IoT topic rule with HTTPS action
    myTopicRule.addAction(
      new aws_iot_actions_alpha.HttpsAction(`${booksApi.url}books`, {})
    );
  }
}

CDK デプロイを行い、addAction により Destination が追加されると、エンドポイントに Confirmation Token が送信されます。HTTPS Actions ではこのトークンを使って Destination の確認を行う必要があります。

{
  resource: '/books',
  path: '/books',
  httpMethod: 'POST',
  headers: {
    'Accept-Encoding': 'gzip,deflate',
    'CloudFront-Forwarded-Proto': 'https',
    'CloudFront-Is-Desktop-Viewer': 'true',
    'CloudFront-Is-Mobile-Viewer': 'false',
    'CloudFront-Is-SmartTV-Viewer': 'false',
    'CloudFront-Is-Tablet-Viewer': 'false',
    'CloudFront-Viewer-ASN': '16509',
    'CloudFront-Viewer-Country': 'JP',
    'Content-Type': 'application/json; charset=UTF-8',
    Host: 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com',
    'User-Agent': 'Apache-HttpClient/UNAVAILABLE (Java/11.0.18)',
    Via: '1.1 d4a3f04c47d13487e5266b80020b9e0c.cloudfront.net (CloudFront)',
    'X-Amz-Cf-Id': 'SdKKdVR8vhExrXBFHw7AEeEFzhQrU11BJl-ce9X1dWbbtHwf1vhZDg==',
    'x-amz-rules-engine-destination-arn': 'arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed',
    'x-amz-rules-engine-message-type': 'DestinationConfirmation',
    'X-Amzn-Trace-Id': 'Root=1-64beacaa-69f5c78a7108b4bc277534a7',
    'X-Forwarded-For': '35.77.218.140, 64.252.172.87',
    'X-Forwarded-Port': '443',
    'X-Forwarded-Proto': 'https'
  },
  multiValueHeaders: {
    'Accept-Encoding': [ 'gzip,deflate' ],
    'CloudFront-Forwarded-Proto': [ 'https' ],
    'CloudFront-Is-Desktop-Viewer': [ 'true' ],
    'CloudFront-Is-Mobile-Viewer': [ 'false' ],
    'CloudFront-Is-SmartTV-Viewer': [ 'false' ],
    'CloudFront-Is-Tablet-Viewer': [ 'false' ],
    'CloudFront-Viewer-ASN': [ '16509' ],
    'CloudFront-Viewer-Country': [ 'JP' ],
    'Content-Type': [ 'application/json; charset=UTF-8' ],
    Host: [ 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com' ],
    'User-Agent': [ 'Apache-HttpClient/UNAVAILABLE (Java/11.0.18)' ],
    Via: [
      '1.1 d4a3f04c47d13487e5266b80020b9e0c.cloudfront.net (CloudFront)'
    ],
    'X-Amz-Cf-Id': [ 'SdKKdVR8vhExrXBFHw7AEeEFzhQrU11BJl-ce9X1dWbbtHwf1vhZDg==' ],
    'x-amz-rules-engine-destination-arn': [
      'arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed'
    ],
    'x-amz-rules-engine-message-type': [ 'DestinationConfirmation' ],
    'X-Amzn-Trace-Id': [ 'Root=1-64beacaa-69f5c78a7108b4bc277534a7' ],
    'X-Forwarded-For': [ '35.77.218.140, 64.252.172.87' ],
    'X-Forwarded-Port': [ '443' ],
    'X-Forwarded-Proto': [ 'https' ]
  },
  queryStringParameters: {
    confirmationToken: '<Confirmation Token>'
  },
  multiValueQueryStringParameters: {
    confirmationToken: [
      '<Confirmation Token>'
    ]
  },
  pathParameters: null,
  stageVariables: null,
  requestContext: {
    resourceId: 'i700xu',
    resourcePath: '/books',
    httpMethod: 'POST',
    extendedRequestId: 'Ik_qvEbitjMFuzQ=',
    requestTime: '24/Jul/2023:16:54:02 +0000',
    path: '/prod/books',
    accountId: 'XXXXXXXXXXXX',
    protocol: 'HTTP/1.1',
    stage: 'prod',
    domainPrefix: 'lsahbg2fvj',
    requestTimeEpoch: 1690217642855,
    requestId: '2d618379-18b9-4fc3-ace6-2ee278fa666e',
    identity: {
      cognitoIdentityPoolId: null,
      accountId: null,
      cognitoIdentityId: null,
      caller: null,
      sourceIp: '35.77.218.140',
      principalOrgId: null,
      accessKey: null,
      cognitoAuthenticationType: null,
      cognitoAuthenticationProvider: null,
      userArn: null,
      userAgent: 'Apache-HttpClient/UNAVAILABLE (Java/11.0.18)',
      user: null
    },
    domainName: 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com',
    apiId: 'lsahbg2fvj'
  },
  body: '{"arn":"arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed","confirmationToken":"<Confirmation Token>","enableUrl":"https://iot.ap-northeast-1.amazonaws.com/confirmdestination/<Confirmation Token>","messageType":"DestinationConfirmation"}',
  isBase64Encoded: false
}

以下記事で紹介されている手順を実施して Destination の確認が行われるまでは HttpAction によるエンドポイントへのアクセスは行えません。

試しに確認前の時点でトピックにメッセージを送信してみます。

aws iot-data publish \
  --topic 'topic/subtopic' \
  --cli-binary-format raw-in-base64-out \
  --payload '{"message":"hello from topic!"}'

すると下記のように HTTPS actions の実行のエラーが errorAction により記録されます。Destination が IN_PROGRESS(処理中)となっているため、HttpAction によるリクエストが失敗しています。

{
  ruleName: 'myTopicRule',
  topic: 'topic/subtopic',
  cloudwatchTraceId: '66035269-cbcc-4556-afc6-4f8702ae7dba',
  clientId: 'N/A',
  base64OriginalPayload: 'eyJtZXNzYWdlIjoiaGVsbG8gZnJvbSB0b3BpYyEifQ==',
  failures: [
    {
      failedAction: 'HttpAction',
      failedResource: 'https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books',
      errorMessage: "HttpAction failed to make a request to the specified endpoint. Destination for confirmationUrl 'https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books' is not enabled. Current status: IN_PROGRESS. Message arrived on: topic/subtopic, Action: http, url: https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books"
    }
  ]
}

Destination の確認および有効化を行ってみます。

まず作成された Destination を確認します。ここでもステータスは IN_PROGRESS となっています。

$ aws iot list-topic-rule-destinations
{
    "destinationSummaries": [
        {
            "arn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed",
            "status": "IN_PROGRESS",
            "createdAt": "2023-07-25T01:54:03.243000+09:00",
            "lastUpdatedAt": "2023-07-25T01:54:03.243000+09:00",
            "statusReason": "Awaiting confirmation. Confirmation message sent on 2023-07-24T16:54:01.842Z. The destination responded with HTTP status code - 200.",
            "httpUrlSummary": {
                "confirmationUrl": "https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books"
            }
        }
    ]
}

次に Destination 作成時に送信された Confirmation Token を使って Destination の確認を行います。

$ aws iot confirm-topic-rule-destination --confirmation-token '<Confirmation Token>'

Destination を再度確認すると、Status が DISABLED に変わりました。

$ aws iot get-topic-rule-destination \
  --arn arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed
{
    "topicRuleDestination": {
        "arn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed",
        "status": "DISABLED",
        "createdAt": "2023-07-25T01:58:03.549000+09:00",
        "lastUpdatedAt": "2023-07-25T01:58:03.549000+09:00",
        "httpUrlProperties": {
            "confirmationUrl": "https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books"
        }
    }
}

次に Destination を有効化します。

$ aws iot update-topic-rule-destination \
  --arn arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed \
  --status ENABLED

Destination を再度確認すると、Status が ENABLED に変わりました。

aws iot get-topic-rule-destination --arn arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed                                                                       
{
    "topicRuleDestination": {
        "arn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:ruledestination/http/ee405999-a98f-4ee2-a4e0-5c84609b93ed",
        "status": "ENABLED",
        "createdAt": "2023-07-25T02:01:09.407000+09:00",
        "lastUpdatedAt": "2023-07-25T02:01:09.407000+09:00",
        "httpUrlProperties": {
            "confirmationUrl": "https://lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com/prod/books"
        }
    }
}

再度トピックにメッセージを送信すると、今度はエンドポイントへのリクエストが行われました。

aws iot-data publish \
  --topic 'topic/subtopic' \
  --cli-binary-format raw-in-base64-out \
  --payload '{"message":"hello from topic!"}'
{
  resource: '/books',
  path: '/books',
  httpMethod: 'POST',
  headers: {
    Accept: '*/*',
    'CloudFront-Forwarded-Proto': 'https',
    'CloudFront-Is-Desktop-Viewer': 'true',
    'CloudFront-Is-Mobile-Viewer': 'false',
    'CloudFront-Is-SmartTV-Viewer': 'false',
    'CloudFront-Is-Tablet-Viewer': 'false',
    'CloudFront-Viewer-ASN': '16509',
    'CloudFront-Viewer-Country': 'JP',
    'content-type': 'application/json; charset=UTF-8',
    Host: 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com',
    'User-Agent': 'AHC/2.1',
    Via: '1.1 049da4ca55b7670f4f1d01ff0ec6e23e.cloudfront.net (CloudFront)',
    'X-Amz-Cf-Id': 'bcznNBOrAH3yA0hcUV6xteAxJGw7GdUnOZkwPJZbln9TSWvHdhxAbg==',
    'X-Amzn-Trace-Id': 'Root=1-64beae7f-2c5fee1a7f12aa397d960461',
    'X-Forwarded-For': '54.250.177.65, 64.252.172.87',
    'X-Forwarded-Port': '443',
    'X-Forwarded-Proto': 'https'
  },
  multiValueHeaders: {
    Accept: [ '*/*' ],
    'CloudFront-Forwarded-Proto': [ 'https' ],
    'CloudFront-Is-Desktop-Viewer': [ 'true' ],
    'CloudFront-Is-Mobile-Viewer': [ 'false' ],
    'CloudFront-Is-SmartTV-Viewer': [ 'false' ],
    'CloudFront-Is-Tablet-Viewer': [ 'false' ],
    'CloudFront-Viewer-ASN': [ '16509' ],
    'CloudFront-Viewer-Country': [ 'JP' ],
    'content-type': [ 'application/json; charset=UTF-8' ],
    Host: [ 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com' ],
    'User-Agent': [ 'AHC/2.1' ],
    Via: [
      '1.1 049da4ca55b7670f4f1d01ff0ec6e23e.cloudfront.net (CloudFront)'
    ],
    'X-Amz-Cf-Id': [ 'bcznNBOrAH3yA0hcUV6xteAxJGw7GdUnOZkwPJZbln9TSWvHdhxAbg==' ],
    'X-Amzn-Trace-Id': [ 'Root=1-64beae7f-2c5fee1a7f12aa397d960461' ],
    'X-Forwarded-For': [ '54.250.177.65, 64.252.172.87' ],
    'X-Forwarded-Port': [ '443' ],
    'X-Forwarded-Proto': [ 'https' ]
  },
  queryStringParameters: null,
  multiValueQueryStringParameters: null,
  pathParameters: null,
  stageVariables: null,
  requestContext: {
    resourceId: 'i700xu',
    resourcePath: '/books',
    httpMethod: 'POST',
    extendedRequestId: 'IlAz-GJYtjMFysg=',
    requestTime: '24/Jul/2023:17:01:51 +0000',
    path: '/prod/books',
    accountId: 'XXXXXXXXXXXX',
    protocol: 'HTTP/1.1',
    stage: 'prod',
    domainPrefix: 'lsahbg2fvj',
    requestTimeEpoch: 1690218111574,
    requestId: 'fa3652d3-a7aa-4efc-bbe4-c02900612154',
    identity: {
      cognitoIdentityPoolId: null,
      accountId: null,
      cognitoIdentityId: null,
      caller: null,
      sourceIp: '54.250.177.65',
      principalOrgId: null,
      accessKey: null,
      cognitoAuthenticationType: null,
      cognitoAuthenticationProvider: null,
      userArn: null,
      userAgent: 'AHC/2.1',
      user: null
    },
    domainName: 'lsahbg2fvj.execute-api.ap-northeast-1.amazonaws.com',
    apiId: 'lsahbg2fvj'
  },
  body: '{"message":"hello from topic!"}',
  isBase64Encoded: false
}

おわりに

AWS CDK v2.88.0 で、IoT rule actions の L2 Construct で HTTPS Actions がサポートされたのでご紹介しました。

HTTPS Actions は今回初めて触ったのですが、Destinations という概念があることや、エンドポイントへリクエスト可能とするためには確認および有効化の操作が必要であることなど、ハマりどころが若干ありました。

参考

以上