[AWS IoT Core] Route53のプライベートホストゾーンを使用して IoT CoreのVPCエンドポイントの名前解決ができるVPCをCDKで作ってみました

2022.07.27

1 はじめに

CX事業本部デリバリー部の平内(SIN)です。

IoT Coreでは、VPCから直接アクセスできるようにVPCエンドポイントをサポートしています。 しかし、VPC内からこのエンドポイントを使用する場合、適切な名前解決が、必要になります。

デフォルトで利用可能なデバイスエンドポイントを使用する場合も、カスタムドメインを使用する場合も、そのFQDNとVPCエンドポイントのローカルアドレスを紐づける必要があります。


参考:VPC エンドポイントの制約事項

今回は、IoT Coreのエンドポイントと、その名前解決ができるVPCをCDKで作成してみました。

2 構成図

CDKで作成されるリソースは、図の通りです。 IoT Coreのエンドポイントを作成し、Route53 のプライベートホストゾーンで名前解決ができるようになっています。

図中のEC2は、動作確認のために設置したもので、CDKのは含まれていません。

3 CDK

CDKのコードです。

IoT Coreのエンドポイントを作成した後、カスタムリソース(Lambda)で、そのエンドポイントのローカルアドレスを取得し、Route53のプライベートホストゾーンを追加しています。

エンドポイントのセキュリティグループでは、とりあえず、VPCのCIDR内からのアクセスを全て許可しています。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as route53 from "aws-cdk-lib/aws-route53";

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

    const cidr = '10.0.0.0/16';
    const iotCoreEndpoint = 'xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com';
    const port = 8883;

    // VPC
    const vpc = new ec2.Vpc(this, 'Vpc', {
      vpcName: `${this.stackName}/vpc`,
      cidr,
      natGateways: 0,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'isolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
      maxAzs: 2,
    });

    // Vpc Endpoint(IoT Core)
    const endpointSecurityGroup = new ec2.SecurityGroup(this, 'EndpointSg', {
      vpc,
      securityGroupName: `${this.stackName}/EndpointSg`,
    });
    endpointSecurityGroup.addIngressRule(ec2.Peer.ipv4(cidr), ec2.Port.tcp(port), 'MQTT from VPC');
    const vpcep = new ec2.CfnVPCEndpoint(this, 'VpcEndpoint', {
      vpcId: vpc.vpcId,
      serviceName: 'com.amazonaws.ap-northeast-1.iot.data',
      vpcEndpointType: ec2.VpcEndpointType.INTERFACE,
      subnetIds: vpc.isolatedSubnets.map(s => s.subnetId),
      securityGroupIds: [endpointSecurityGroup.securityGroupId]
    });

    // VPC Endpointのローカルアドレス取得
    const vpcEndpointAddressList = getVpcEndpointAddressList(this, vpcep);

    // Local DNS(IoT CoreのエンドポイントをVPC Endpointのローカルアドレスに紐付ける)
    const iotCoreDomain = iotCoreEndpoint.substring(iotCoreEndpoint.indexOf('.') + 1);
    const iotCoreZone = new route53.CfnHostedZone(this, `host-zone-${iotCoreDomain}`, {
      vpcs: [{
        vpcId: vpc.vpcId,
        vpcRegion: this.region,
      }],
      name: iotCoreDomain
    });
    new route53.CfnRecordSet(this, `recordSet-${iotCoreDomain}`, {
      name: iotCoreEndpoint,
      type: 'A',
      setIdentifier: iotCoreEndpoint,
      hostedZoneId: iotCoreZone.attrId,
      region: this.region,
      resourceRecords: vpcEndpointAddressList,
      ttl: '300'
    });
  }
}

function getVpcEndpointAddressList(scope: Construct, vpcep: ec2.CfnVPCEndpoint): string[] {

  const role = new iam.Role(scope, 'GetEndpointAddressList', {
    roleName: 'GetEndpointAddressList',
    assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    inlinePolicies: {
      'VPCEndpointIPsLambdaPolicy': new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'ec2:DescribeNetworkInterfaces',
              'ec2:DescribeNetworkInterfaceAttribute',
            ],
            resources: ['*']
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'logs:CreateLogGroup',
              'logs:CreateLogStream',
              'logs:PutLogEvents',
            ],
            resources: [cdk.Fn.sub('arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-*')]
          })
        ]
      })
    }
  });

  const crFunction = new lambda.CfnFunction(scope, `LambdaFunction`, {
    handler: 'index.lambda_handler',
    runtime: 'python3.9',
    timeout: 10,
    role: role.roleArn,
    code: {
      zipFile: `
import cfnresponse
import boto3
import json
def lambda_handler(event, context):
  responseStatus = cfnresponse.FAILED
  responseData = {}
  if 'RequestType' not in event:
    responseData = {'error': 'RequestType not in event'}
  elif event['RequestType'] == 'Delete':
    responseStatus = cfnresponse.SUCCESS
  elif event['RequestType'] in ['Create', 'Update']:
    try:
      responseData['IPs'] = []
      ec2 = boto3.resource('ec2')
      eni_ids = event['ResourceProperties']['NetworkInterfaceIds']
      for eni_id in eni_ids:
        eni = ec2.NetworkInterface(eni_id)
        responseData['IPs'].append(eni.private_ip_address)
      responseStatus = cfnresponse.SUCCESS
    except Exception as e:
      responseData = {'error': str(e)}
  cfnresponse.send(event, context, responseStatus, responseData)
`}
  });

  const result = new cdk.CustomResource(scope, `CustomResorce`, {
    serviceToken: crFunction.attrArn,
    properties: {
      NetworkInterfaceIds: vpcep.attrNetworkInterfaceIds,
    }
  });
  const addressList: string[] = [];
  addressList.push(cdk.Fn.select(0, result.getAtt('IPs').toString() as unknown as string[]));
  addressList.push(cdk.Fn.select(1, result.getAtt('IPs').toString() as unknown as string[]));
  return addressList;
}

4 動作確認

CDKで作成したVPC内にEC2を立ち上げて、動作確認してみました。

(1) 名前解決

IoT Coreのエンドポイントの名前解決を確認すると、ローカルアドレスである10.0.2.149及び、10.0.3.71となっていることが確認できます。

[ec2-user@ip-10-0-0-62 ~]$ dig xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com

; <> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <> xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21493
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com. IN  A

;; ANSWER SECTION:
xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com. 300 IN A 10.0.2.149
xxxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com. 300 IN A 10.0.3.71

;; Query time: 3 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: 水  7月 27 01:47:47 UTC 2022
;; MSG SIZE  rcvd: 112

(2) MQTTクライアントによる接続

接続キットで簡単に、MQTTの接続を試してみました。

特に違和感なく、接続できていることが確認できます。

また、IoT Coreのログを確認すると、EC2のローカルアドレス(10.0.0.62)が、ソースアドレスになっている事も分かります。

5 最後に

今回は、IoT Coreのエンドポイントが、VPC内から簡単に利用できるように、Route53の名前解決を含んだVPCを作成するサンプルを作ってみました。

VPC内からIoT Coreのエンドポイントを使用する場合の参考になれば幸いです。