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アカウントにはデプロイしません。 また、今回はアプリケーション自体をデプロイするスタックは説明しません。
手順
おおまかな流れです。
- ピアリングに必要なDBアカウントの情報を取得
- DBアカウント側でクロスアカウントでVPC Peeringを受け入れるためのクロスアカウントロールを作成
- CDKでAPアカウントにVPCおよびVPC Peeringをデプロイ
- VPCの作成
- VPC Peeringの作成
- Route TableでDBアカウントVPCへのrouteを設定
- DNS Resolutionの設定
- 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化できたので、ネットワーク部分のコード化もより進められそうです。