CDKでクロスアカウント VPC Peering with DNS Resolution

TL;DR

  • CDKでクロスアカウントのVPCにVPC Peeringができる
  • DNS ResolutionもCDKの中で反映できる

経緯

アプリケーションをCDKでデプロイする時、既存のVPCリソースを使いたい場合がよくあります。 例えば別VPCにあるRDSをアプリケーションから使いたいとか。

こういった時にVPC Peeringは1つの選択肢だと思いますが、これをCDKで実施できないかと試してみました。 今回は既存の別AWSアカウントが保有するVPCに対してCDKでピアリングします。

今回のケース

  • 既存のAWSアカウント(以降、DBアカウント)が存在する

  • DBアカウントにはVPCがあり、その中にRDSが配置されている

  • アプリケーションをデプロイするAWSアカウント(以降、APアカウント)が存在する

  • アプリケーションを配置するVPCはこれからCDKで作成する

  • APアカウントのVPCはDBアカウントのVPCにピアリングを張ってアプリケーションからRDSに接続したい

  • APアカウントからDB接続する際はRDSのエンドポイント名を使って接続したい

    • VPC間でDNS解決ができるようにしたい

上記のようなケースでAPアカウント側にCDKでデプロイしようという感じです。 CDKはAPアカウント上にだけデプロイし、DBアカウントにはデプロイしません。 また、今回はアプリケーション自体をデプロイするスタックは説明しません。

手順

おおまかな流れです。

  1. ピアリングに必要なDBアカウントの情報を取得
  2. DBアカウント側でクロスアカウントでVPC Peeringを受け入れるためのクロスアカウントロールを作成
  3. CDKでAPアカウントにVPCおよびVPC Peeringをデプロイ
    • VPCの作成
    • VPC Peeringの作成
    • Route TableでDBアカウントVPCへのrouteを設定
    • DNS Resolutionの設定
  4. DBアカウント側でRoute TableとSecurity Groupの編集

1. ピアリングに必要なDBアカウントの情報を取得

VPCピアリング接続の設定にDBアカウントの以下情報が必要になりますので、メモしておきます。

  • DBアカウントのAWS アカウントID
  • DBアカウントが保有しているVPC(RDSが配置されているVPC)のVPC ID
  • DBアカウントが保有しているVPC(RDSが配置されているVPC)のリージョン

2. DBアカウント側でクロスアカウントでVPC Peeringを受け入れるためのクロスアカウントロールを作成

今回はCLIで実行しました。

ポイントはAPアカウントからのクロスアカウントでのAssumeRoleを許可して policyに ec2:AcceptVpcPeeringConnection ec2:ModifyVpcPeeringConnectionOptions の実行許可を与えるところです。

この2つのアクセス権により、APアカウントからDBアカウントへのピアリングリクエスト・DNSResolution変更リクエストを許可します。

 1ROLE_NAME=accept-vpc-peering-from-ap-account-role
 2
 3aws iam create-role \
 4  --role-name $ROLE_NAME \
 5  --assume-role-policy-document \
 6'{
 7    "Version": "2012-10-17",
 8    "Statement": [
 9        {
10            "Effect": "Allow",
11            "Principal": {
12                "AWS": [
13                  "arn:aws:iam::[AP Account ID]:root"
14                ]
15            },
16            "Action": "sts:AssumeRole"
17        }
18    ]
19}'
20
21aws iam put-role-policy \
22  --role-name $ROLE_NAME \
23  --policy-name accept-vpc-peering-policy \
24  --policy-document \
25'{
26    "Version": "2012-10-17",
27    "Statement": [
28        {
29            "Effect": "Allow",
30            "Action": ["ec2:AcceptVpcPeeringConnection", "ec2:ModifyVpcPeeringConnectionOptions"],
31            "Resource": "*"
32        }
33    ]
34}'

ここで作成したrole名はCDK実行時に必要なのでメモしておきます。

3. CDKでAPアカウントにVPCおよびVPC Peeringをデプロイ

ここが本題です。

まずCDKで作成したstackの全量です。

この中で以下をやっています

  • VPCの作成
  • VPC Peeringの作成
  • Route TableでDBアカウントVPCへのrouteを設定
  • DNS Resolutionの設定
  1import * as cdk from 'aws-cdk-lib'
  2import { Construct } from 'constructs'
  3import { Config } from '@/lib/util/getConfig'
  4import { custom_resources } from 'aws-cdk-lib'
  5
  6interface Props extends cdk.StackProps {
  7  readonly config: Config
  8}
  9
 10export class VpcStack extends cdk.Stack {
 11  constructor(scope: Construct, id: string, props: Props) {
 12    super(scope, id, props)
 13
 14    const { config } = props
 15
 16    const vpc = new cdk.aws_ec2.Vpc(this, 'vpc', {
 17      ipAddresses: cdk.aws_ec2.IpAddresses.cidr(config.VPC_CIDR),
 18      maxAzs: 2,
 19      natGateways: 1,
 20      vpcName: `${config.APP_NAME}-vpc`,
 21      enableDnsHostnames: true,
 22      enableDnsSupport: true,
 23      subnetConfiguration: [
 24        {
 25          name: `${config.APP_NAME}-public-subnet`,
 26          subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
 27          cidrMask: 24,
 28        },
 29        {
 30          name: `${config.APP_NAME}-private-subnet`,
 31          subnetType: cdk.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
 32          cidrMask: 24,
 33        },
 34      ],
 35    })
 36
 37    // peering部分
 38    const peeringConnection = new cdk.aws_ec2.CfnVPCPeeringConnection(
 39      this,
 40      'RequesterAcceptorPeering',
 41      {
 42        vpcId: vpc.vpcId,
 43        // ここでDBアカウントからメモしておいた情報を設定
 44        peerVpcId: config.PEERING_VPC_ID,
 45        peerOwnerId: config.PEERING_OWNER_ID,
 46        peerRegion: config.PEERING_REGION,
 47        peerRoleArn: config.PEERING_ROLE_ARN,
 48        tags: [
 49          {
 50            key: 'Name',
 51            value: `${config.APP_NAME}-peering`,
 52          },
 53        ],
 54      }
 55    )
 56
 57    // DBアカウントのCIDRはピアリングに飛ばすようにroute設定
 58    // これによりRDSに向けた接続リクエストはVPCピアリングを経由してDBアカウントのVPCにいく
 59    // subnet routing to peer vpc
 60    vpc.privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
 61      const route = new cdk.aws_ec2.CfnRoute(
 62        this,
 63        `PrivateSubnetPeeringConnectionRoute-${index}`,
 64        {
 65          destinationCidrBlock: config.PEERING_CIDR,
 66          routeTableId,
 67          vpcPeeringConnectionId: peeringConnection.ref,
 68        }
 69      )
 70      route.addDependency(peeringConnection)
 71    })
 72
 73    // DNS Resolutionの設定
 74    // これにより、APアカウントのアプリケーションからDBアカウントのRDSに接続する時、
 75    // RDSのエンドポイント名を指定することができる
 76    // (DNS解決されてDBアカウントのprivate ipに変換される)
 77    // peering dns resolution(use custom resource)
 78    const modifyDnsResolution = (
 79      scope: Construct,
 80      target: 'acceptor' | 'requester',
 81      config: Config
 82    ) => {
 83      const onCreate: custom_resources.AwsSdkCall =
 84        target === 'acceptor'
 85          ? {
 86              assumedRoleArn: config.PEERING_ROLE_ARN,
 87              service: 'EC2',
 88              action: 'modifyVpcPeeringConnectionOptions',
 89              parameters: {
 90                VpcPeeringConnectionId: peeringConnection.ref,
 91                AccepterPeeringConnectionOptions: {
 92                  AllowDnsResolutionFromRemoteVpc: true,
 93                },
 94              },
 95              physicalResourceId: custom_resources.PhysicalResourceId.of(
 96                `${config.APP_NAME}-allowVPCPeeringDNSResolution-acceptor`
 97              ),
 98            }
 99          : {
100              service: 'EC2',
101              action: 'modifyVpcPeeringConnectionOptions',
102              parameters: {
103                VpcPeeringConnectionId: peeringConnection.ref,
104                RequesterPeeringConnectionOptions: {
105                  AllowDnsResolutionFromRemoteVpc: true,
106                },
107              },
108              physicalResourceId: custom_resources.PhysicalResourceId.of(
109                `${config.APP_NAME}-allowVPCPeeringDNSResolution-requester`
110              ),
111            }
112
113      const onUpdate = onCreate
114      const onDelete: custom_resources.AwsSdkCall =
115        target === 'acceptor'
116          ? {
117              assumedRoleArn: config.PEERING_ROLE_ARN,
118              service: 'EC2',
119              action: 'modifyVpcPeeringConnectionOptions',
120              parameters: {
121                VpcPeeringConnectionId: peeringConnection.ref,
122                AccepterPeeringConnectionOptions: {
123                  AllowDnsResolutionFromRemoteVpc: false,
124                },
125              },
126            }
127          : {
128              service: 'EC2',
129              action: 'modifyVpcPeeringConnectionOptions',
130              parameters: {
131                VpcPeeringConnectionId: peeringConnection.ref,
132                RequesterPeeringConnectionOptions: {
133                  AllowDnsResolutionFromRemoteVpc: false,
134                },
135              },
136            }
137
138      const customResource = new custom_resources.AwsCustomResource(
139        scope,
140        `allow-peering-dns-resolution-${target}`,
141        {
142          policy: custom_resources.AwsCustomResourcePolicy.fromStatements([
143            new cdk.aws_iam.PolicyStatement({
144              effect: cdk.aws_iam.Effect.ALLOW,
145              resources: ['*'],
146              actions: ['ec2:ModifyVpcPeeringConnectionOptions'],
147            }),
148            new cdk.aws_iam.PolicyStatement({
149              effect: cdk.aws_iam.Effect.ALLOW,
150              resources: ['*'],
151              actions: ['sts:AssumeRole'],
152            }),
153          ]),
154          logRetention: cdk.aws_logs.RetentionDays.ONE_DAY,
155          onCreate,
156          onUpdate,
157          onDelete,
158        }
159      )
160
161      customResource.node.addDependency(peeringConnection)
162    }
163
164    // DNS ResolutionをDBアカウント側に設定
165    // for acceptor peering dns resolutiion
166    modifyDnsResolution(this, 'acceptor', config)
167
168    // DNS ResolutionをAPアカウント側に設定
169    // for requester peering dns resolutiion
170    modifyDnsResolution(this, 'requester', config)
171
172    // 後続のstackで作成したVPCを参照できるようにSSM Parameterにexportしておく
173    // export
174    new cdk.aws_ssm.StringParameter(this, `SSMParamaterVpcId`, {
175      stringValue: vpc.vpcId,
176      parameterName: `/${config.APP_NAME}/VPC/VPCID`,
177    })
178  }
179}

ポイントを見ていきます。

 1// peering部分
 2const peeringConnection = new cdk.aws_ec2.CfnVPCPeeringConnection(
 3  this,
 4  'RequesterAcceptorPeering',
 5  {
 6    vpcId: vpc.vpcId,
 7    // ここでDBアカウントからメモしておいた情報を設定
 8    peerVpcId: config.PEERING_VPC_ID,
 9    peerOwnerId: config.PEERING_OWNER_ID,
10    peerRegion: config.PEERING_REGION,
11    peerRoleArn: config.PEERING_ROLE_ARN,
12    tags: [
13      {
14        key: 'Name',
15        value: `${config.APP_NAME}-peering`,
16      },
17    ],
18  }
19)

VPC Peeringを作成する部分です。 今回はクロスアカウントのため、Peering先となるDBアカウントの情報をセットしています。 この記述だけでAPアカウント側・DBアカウント側の両方にVPC Peeringがリソースとして作成されます。

次にroute tableの更新です。

 1// DBアカウントのCIDRはピアリングに飛ばすようにroute設定
 2// これによりRDSに向けた接続リクエストはVPCピアリングを経由してDBアカウントのVPCにいく
 3// subnet routing to peer vpc
 4vpc.privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
 5  const route = new cdk.aws_ec2.CfnRoute(
 6    this,
 7    `PrivateSubnetPeeringConnectionRoute-${index}`,
 8    {
 9      destinationCidrBlock: config.PEERING_CIDR,
10      routeTableId,
11      vpcPeeringConnectionId: peeringConnection.ref,
12    }
13  )
14  route.addDependency(peeringConnection)
15})

VPC Peeringを設定した後、DBアカウントのCIDRに対するリクエストをDBアカウントのVPCに回すために Route Tableの設定が必要となります。 上記のようにsubnet毎のRouteTableにRouteを追加します。

最後にDNS Resolutionの部分です。

CDKではDNS Resolutionの設定に関するコンストラクタがなさそうなので CustomResourceを使ってAWS SDKコマンドを実行しているところがポイントです。

 1// DNS Resolutionの設定
 2// これにより、APアカウントのアプリケーションからDBアカウントのRDSに接続する時、
 3// RDSのエンドポイント名を指定することができる
 4// (DNS解決されてDBアカウントのprivate ipに変換される)
 5// peering dns resolution(use custom resource)
 6const modifyDnsResolution = (
 7  scope: Construct,
 8  target: 'acceptor' | 'requester',
 9  config: Config
10) => {
11  const onCreate: custom_resources.AwsSdkCall =
12    target === 'acceptor'
13      ? {
14          assumedRoleArn: config.PEERING_ROLE_ARN,
15          service: 'EC2',
16          action: 'modifyVpcPeeringConnectionOptions',
17          parameters: {
18            VpcPeeringConnectionId: peeringConnection.ref,
19            AccepterPeeringConnectionOptions: {
20              AllowDnsResolutionFromRemoteVpc: true,
21            },
22          },
23          physicalResourceId: custom_resources.PhysicalResourceId.of(
24            `${config.APP_NAME}-allowVPCPeeringDNSResolution-acceptor`
25          ),
26        }
27      : {
28          service: 'EC2',
29          action: 'modifyVpcPeeringConnectionOptions',
30          parameters: {
31            VpcPeeringConnectionId: peeringConnection.ref,
32            RequesterPeeringConnectionOptions: {
33              AllowDnsResolutionFromRemoteVpc: true,
34            },
35          },
36          physicalResourceId: custom_resources.PhysicalResourceId.of(
37            `${config.APP_NAME}-allowVPCPeeringDNSResolution-requester`
38          ),
39        }
40
41  const onUpdate = onCreate
42  const onDelete: custom_resources.AwsSdkCall =
43    target === 'acceptor'
44      ? {
45          assumedRoleArn: config.PEERING_ROLE_ARN,
46          service: 'EC2',
47          action: 'modifyVpcPeeringConnectionOptions',
48          parameters: {
49            VpcPeeringConnectionId: peeringConnection.ref,
50            AccepterPeeringConnectionOptions: {
51              AllowDnsResolutionFromRemoteVpc: false,
52            },
53          },
54        }
55      : {
56          service: 'EC2',
57          action: 'modifyVpcPeeringConnectionOptions',
58          parameters: {
59            VpcPeeringConnectionId: peeringConnection.ref,
60            RequesterPeeringConnectionOptions: {
61              AllowDnsResolutionFromRemoteVpc: false,
62            },
63          },
64        }
65
66  const customResource = new custom_resources.AwsCustomResource(
67    scope,
68    `allow-peering-dns-resolution-${target}`,
69    {
70      policy: custom_resources.AwsCustomResourcePolicy.fromStatements([
71        new cdk.aws_iam.PolicyStatement({
72          effect: cdk.aws_iam.Effect.ALLOW,
73          resources: ['*'],
74          actions: ['ec2:ModifyVpcPeeringConnectionOptions'],
75        }),
76        new cdk.aws_iam.PolicyStatement({
77          effect: cdk.aws_iam.Effect.ALLOW,
78          resources: ['*'],
79          actions: ['sts:AssumeRole'],
80        }),
81      ]),
82      logRetention: cdk.aws_logs.RetentionDays.ONE_DAY,
83      onCreate,
84      onUpdate,
85      onDelete,
86    }
87  )
88
89  customResource.node.addDependency(peeringConnection)
90}
91
92// DNS ResolutionをDBアカウント側に設定
93// for acceptor peering dns resolutiion
94modifyDnsResolution(this, 'acceptor', config)
95
96// DNS ResolutionをAPアカウント側に設定
97// for requester peering dns resolutiion
98modifyDnsResolution(this, 'requester', config)

また、パラメータで assumedRoleArn をつけたり、つけなかったりしている部分もポイントです。 DNS Resolutionの設定はAPアカウント側とDBアカウント側の両方に行う必要があります。

APアカウント側はCDKの中でCustomResourceの実行ロールに対して Policyで ec2:ModifyVpcPeeringConnectionOptions を付与しているので CustomResourceからSDKを発行すれば反映できます。

一方で、DBアカウント側はCustomResourceの実行ロールでなく、クロスアカウントロールを利用して SDKを発行する必要があります(CDKを実行しているのはAPアカウント側だからです)

そのため、APアカウント側に実行する場合と、DBアカウント側に実行する場合とで assumedRoleArn を指定したり、指定しなかったりしています。

assumedRoleArn を指定すると、指定したロールを利用してSDKを発行してくれます。 なので assumedRoleArn に DBアカウントで作成したクロスアカウントロールのARNを指定します。

4. DBアカウント側でRoute TableとSecurity Groupの編集

最後にDBアカウント側のRouteTableとSecurity Groupを編集します。

  • Route Table

    • APアカウント側VPCと通信する対象のサブネットのRouteTableを編集
    • CIDRがAPアカウントのVPC CIDRに対してVPCピアリングにRouteするようにする
  • Security Group

    • RDSのSecurity Groupを編集
    • APアカウントのVPC CIDRからのデータベース接続(ポート)を許可

まとめ

CDKでクロスアカウントのVPC Peeringを行うことができました。 クロスアカウントのPeeringの場合、両方のアカウントのリソース操作が発生するため 実行ロールが誰で、影響を与えるリソースが何なのかをよく理解しないと すぐに権限なしエラーに嵌ります(嵌りました)

ネットでもCDKでの実行は事例が少なく苦労しましたが Peering部分もCDK化できたので、ネットワーク部分のコード化もより進められそうです。