先日発表されたAmazonAuroraのIPv6をAWS CDKで試してみた
はじめに
先日、Amazon AuroraのIPv6サポートが発表されました。
私はこれまでの経験上IPv6を意識したことはありませんでしたが、Egress-Only Internet Gateway (EGW)は無料[1]で利用できるということもあり「IPv6だけで構築できればNATゲートウェイのコストを削減して安く構築できるのでは!?」といった邪な発想が頭によぎり、これを機にIPv6環境を構築してみることにしました。
注意点
Amazon AuroraのIPv6サポートをするにはAuroraクラスターのNetworkTypeをDual-stack modeに切り替える必要があります。Dual-stack modeには次の条件があるためご注意ください。
- Aurora MySQL バージョン:
- 3.02.0 以降
- Aurora PostgreSQL のバージョン:
- バージョン14は14.3以降
- バージョン13は13.7以降
- IPv6のみに制限することはできません。IPv4とIPv6の両方が使用できる状態になります。
- db.r3インスタンスクラスはサポートされていません。
- パブリックアクセスは許可できません。
- RDS Proxyが利用できません。
詳しくはAWSの公式ドキュメントを確認してください
構築する
次のような構成をCDKで作成してみます。
先に説明しておきますが、CDKでIPv6環境を構築するのは少々手間です。
私は普段からCDKを愛用して推奨していますが、正直なところv2.39.0の段階ではIPv6環境に関して回避策を求められることが多く、CDKの簡単かつ直感的に記述できる良さが出ていないと思っています。
IPv6に対応したVPC
まず、VPCからIPv6用に対処が必要です。
AWS CDKのVPC L2コンストラクトだけではIPv6に対応することができません。
そのため、こちらのIssueを参考にさせていただいてIPv6に対応したVPCのL2.5コンストラクトを作成しました。
import { Fn } from 'aws-cdk-lib';
import {
Vpc,
CfnVPCCidrBlock,
CfnInternetGateway,
Subnet,
RouterType,
CfnSubnet,
VpcProps,
} from 'aws-cdk-lib/aws-ec2';
import { Construct, IConstruct } from 'constructs';
/**
* Gets a value or throws an exception.
*
* @param value A value, possibly undefined
* @param err The error to throw if `value` is undefined.
*/
const valueOrDie = <T, C extends T = T>(
value: T | undefined,
err: Error
): C => {
if (value === undefined) throw err;
return value as C;
};
export interface Ipv6VpcProps extends VpcProps {}
/**
* IPv6 support VPC.
*
* For example:
* ```
* const vpc = new Ipv6Vpc(this, "Ipv6Vpc", { ... });
* ```
*/
export class Ipv6Vpc extends Vpc {
constructor(scope: Construct, id: string, props: VpcProps) {
super(scope, id, props);
// Associate an IPv6 block with the VPC.
// Note: You're may get an error like, "The network 'your vpc id' has met
// its maximum number of allowed CIDRs" if you cause this
// `AWS::EC2::VPCCidrBlock` ever to be recreated.
const ipv6Cidr = new CfnVPCCidrBlock(this, 'IPv6Cidr', {
vpcId: this.vpcId,
amazonProvidedIpv6CidrBlock: true,
});
// Get the vpc's internet gateway so we can create default routes for the
// public subnets.
const internetGateway = valueOrDie<IConstruct, CfnInternetGateway>(
this.node.children.find((c) => c instanceof CfnInternetGateway),
new Error("Couldn't find an internet gateway")
);
// Modify each public subnet so that it has both a public route and an ipv6
// CIDR.
this.publicSubnets.forEach((subnet, idx) => {
// Add a default ipv6 route to the subnet's route table.
const unboxedSubnet = subnet as Subnet;
unboxedSubnet.addRoute('IPv6Default', {
routerId: internetGateway.ref,
routerType: RouterType.GATEWAY,
destinationIpv6CidrBlock: '::/0',
});
// Find a CfnSubnet (raw cloudformation resources) child to the public
// subnet nodes.
const cfnSubnet = valueOrDie<IConstruct, CfnSubnet>(
subnet.node.children.find((c) => c instanceof CfnSubnet),
new Error("Couldn't find a CfnSubnet")
);
// Use the intrinsic Fn::Cidr CloudFormation function on the VPC's
// first IPv6 block to determine ipv6 /64 cidrs for each subnet as
// a function of the public subnet's index.
const vpcCidrBlock = Fn.select(0, this.vpcIpv6CidrBlocks);
const ipv6Cidrs = Fn.cidr(vpcCidrBlock, 256, '64');
cfnSubnet.ipv6CidrBlock = Fn.select(idx, ipv6Cidrs);
// The subnet depends on the ipv6 cidr being allocated.
cfnSubnet.addDependsOn(ipv6Cidr);
});
// Modify each private subnet so that it has both a public route and an ipv6
// CIDR.
this.isolatedSubnets.forEach((subnet, idx) => {
// Add a default ipv6 route to the subnet's route table.
const unboxedSubnet = subnet as Subnet;
unboxedSubnet.addRoute('IPv6Default', {
routerId: internetGateway.ref,
routerType: RouterType.GATEWAY,
destinationIpv6CidrBlock: '::/0',
});
// Find a CfnSubnet (raw cloudformation resources) child to the public
// subnet nodes.
const cfnSubnet = valueOrDie<IConstruct, CfnSubnet>(
subnet.node.children.find((c) => c instanceof CfnSubnet),
new Error("Couldn't find a CfnSubnet")
);
// Use the intrinsic Fn::Cidr CloudFormation function on the VPC's
// first IPv6 block to determine ipv6 /64 cidrs for each subnet as
// a function of the private subnet's index.
const vpcCidrBlock = Fn.select(0, this.vpcIpv6CidrBlocks);
const ipv6Cidrs = Fn.cidr(vpcCidrBlock, 256, '64');
cfnSubnet.ipv6CidrBlock = Fn.select(idx + 3, ipv6Cidrs);
// The subnet depends on the ipv6 cidr being allocated.
cfnSubnet.addDependsOn(ipv6Cidr);
});
}
}
AuroraをDual-stack modeに対応させる
VPCに続き、AuroraもNetworkTypeをDual-stack modeにする対応が必要です。
こちらに関してはCloudFormation自体がまだNetworkTypeのプロパティに対応していないため、一度Auroraクラスターを作成した後にカスタムリソースを利用してNetworkTypeを変更しなければなりません。
NetworkTypeを変更するためのコンストラクトも作成します。
import { Arn, ArnFormat, Stack } from 'aws-cdk-lib';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { IDatabaseCluster } from 'aws-cdk-lib/aws-rds';
import {
AwsCustomResource,
AwsCustomResourcePolicy,
PhysicalResourceId,
} from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
export interface DualStackModeProps {
cluster: IDatabaseCluster;
}
export class DualStackMode extends Construct {
constructor(scope: Construct, id: string, props: DualStackModeProps) {
super(scope, id);
new AwsCustomResource(this, 'DualStackMode', {
resourceType: 'Custom::DualStackMode',
onCreate: {
action: 'modifyDBCluster',
service: 'RDS',
parameters: {
DBClusterIdentifier: props.cluster.clusterIdentifier,
NetworkType: 'DUAL',
ApplyImmediately: true,
},
physicalResourceId: PhysicalResourceId.of('DualStackMode'),
},
onUpdate: {
action: 'modifyDBCluster',
service: 'RDS',
parameters: {
DBClusterIdentifier: props.cluster.clusterIdentifier,
NetworkType: 'DUAL',
ApplyImmediately: true,
},
physicalResourceId: PhysicalResourceId.of('DualStackMode'),
},
onDelete: {
action: 'modifyDBCluster',
service: 'RDS',
parameters: {
DBClusterIdentifier: props.cluster.clusterIdentifier,
NetworkType: 'IPV4',
ApplyImmediately: true,
},
physicalResourceId: PhysicalResourceId.of('DualStackMode'),
},
policy: AwsCustomResourcePolicy.fromStatements([
new PolicyStatement({
actions: ['rds:ModifyDBCluster'],
resources: [
Arn.format(
{
service: 'rds',
resource: 'cluster',
resourceName: props.cluster.clusterIdentifier,
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
},
Stack.of(this)
),
Arn.format(
{
service: 'rds',
resource: 'cluster-pg',
resourceName: '*',
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
},
Stack.of(this)
),
Arn.format(
{
service: 'rds',
resource: 'og',
resourceName: '*',
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
},
Stack.of(this)
),
],
}),
]),
});
}
}
スタックを作成
先ほど作成したコンストラクトを組み合わせて次のリソース群を作成します。
- IPv6に対応したVPC
- IPv4のみのAurora(比較用)
- Dual-stack modeに対応したAuroraクラスター
- IPv6に対応したEC2インスタンス(検証用)
CDKコードは次の様になります。
import { App, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import {
AmazonLinuxCpuType,
AmazonLinuxGeneration,
Instance,
InstanceClass,
InstanceSize,
InstanceType,
MachineImage,
Peer,
Port,
SubnetType,
} from 'aws-cdk-lib/aws-ec2';
import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import {
AuroraMysqlEngineVersion,
DatabaseCluster,
DatabaseClusterEngine,
} from 'aws-cdk-lib/aws-rds';
import { Construct } from 'constructs';
import { DualStackMode } from './lib/dual-stack-mode';
import { Ipv6Vpc } from './lib/ipv6-vpc';
export class Ipv6AuroraStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
const vpc = new Ipv6Vpc(this, 'VPC', {
natGateways: 0,
maxAzs: 2,
});
const ipv6AuroraCluster = new DatabaseCluster(this, 'IPv6Cluster', {
engine: DatabaseClusterEngine.auroraMysql({
version: AuroraMysqlEngineVersion.VER_3_02_0,
}),
instances: 1,
instanceProps: {
vpc,
vpcSubnets: {
subnetType: SubnetType.PRIVATE_ISOLATED,
},
},
removalPolicy: RemovalPolicy.DESTROY,
});
new DualStackMode(this, 'DualStackMode', {
cluster: ipv6AuroraCluster,
});
const ipv4AuroraCluster = new DatabaseCluster(this, 'IPv4Cluster', {
engine: DatabaseClusterEngine.auroraMysql({
version: AuroraMysqlEngineVersion.VER_3_02_0,
}),
instances: 1,
instanceProps: {
vpc,
vpcSubnets: {
subnetType: SubnetType.PRIVATE_ISOLATED,
},
},
removalPolicy: RemovalPolicy.DESTROY,
});
const client = new Instance(this, 'MySQLClient', {
instanceType: InstanceType.of(
InstanceClass.BURSTABLE4_GRAVITON,
InstanceSize.NANO
),
machineImage: MachineImage.latestAmazonLinux({
cpuType: AmazonLinuxCpuType.ARM_64,
generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
}),
vpc,
vpcSubnets: { subnetType: SubnetType.PUBLIC },
role: new Role(this, 'InstanceRole', {
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName(
'AmazonSSMManagedInstanceCore'
),
],
}),
allowAllOutbound: false,
});
client.instance.ipv6AddressCount = 1;
client.connections.allowTo(Peer.anyIpv4(), Port.tcp(80));
client.connections.allowTo(Peer.anyIpv4(), Port.tcp(443));
ipv6AuroraCluster.connections.allowDefaultPortFrom(client);
ipv4AuroraCluster.connections.allowDefaultPortFrom(client);
}
}
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
const app = new App();
new Ipv6AuroraStack(app, 'IPv6AuroraStack', { env });
app.synth();
allowAllOutboundをfalseにする理由
EC2インスタンスのallowAllOutbound
にfalse
を指定していますがこれには理由があります。
現在のCDK(v2.39.0)はallowAllOutbound
をtrue
にするとIPv6のアウトバウンドトラフィックが許可されないという不具合があるためです。この不具合を回避するために個別にアウトバウンドトラフィックを許可しています。今回の構成ではIssueに記載されている回避策はうまく動きませんでした。
検証してみる
リソースが作成できたら実際にIPv6で通信できるか試してみます。
IPv4モードのAレコード
まずはIPv4にしか対応していないAuroraクラスターに対してdigコマンドを実行してみます。
[ssm-user@ip-10-0-26-43 bin]$ dig ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <<>> ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37843
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. IN A
;; ANSWER SECTION:
ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ipv6-aurora-stack-ipv4clusterinstance1e0a2c4b4-o1mhx9bipblm.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com.
ipv6-aurora-stack-ipv4clusterinstance1e0a2c4b4-o1mhx9bipblm.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN A 10.0.205.184
;; Query time: 1 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Sun Aug 28 13:38:56 UTC 2022
;; MSG SIZE rcvd: 236
ANSWER SECTION
を確認するとAレコードにIPv4の値が確認できます。
IPv4モードのAAAAレコード
次に、もう一度IPv4にしか対応していないAuroraクラスターに対してaaaa
オプションをつけたdigコマンドを実行してみます。
[ssm-user@ip-10-0-26-43 bin]$ dig ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com aaaa
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <<>> ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com aaaa
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35273
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. IN AAAA
;; ANSWER SECTION:
ipv6-aurora-stack-ipv4cluster8857ab57-au2fjte2yxh4.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ipv6-aurora-stack-ipv4clusterinstance1e0a2c4b4-o1mhx9bipblm.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com.
;; AUTHORITY SECTION:
ap-northeast-1.rds.amazonaws.com. 58 IN SOA ns-1396.awsdns-46.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
;; Query time: 1 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Sun Aug 28 13:38:58 UTC 2022
;; MSG SIZE rcvd: 302
このクラスターはIPv4にしか対応していないため、ANSWER SECTION
にAAAAレコードがないのが分かります。
DUALモードのAレコード
続いてDual-stack modeに対応させたAuroraクラスターに対してdigコマンドを実行してみます。
[ssm-user@ip-10-0-26-43 bin]$ dig ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <<>> ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28322
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. IN A
;; ANSWER SECTION:
ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ipv6-aurora-stack-ipv6clusterinstance1e72b1cb2-ddtthpmgkzla.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com.
ipv6-aurora-stack-ipv6clusterinstance1e72b1cb2-ddtthpmgkzla.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN A 10.0.195.55
;; Query time: 1 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Sun Aug 28 13:39:46 UTC 2022
;; MSG SIZE rcvd: 236
先ほどと同様にIPv4アドレスが問題なく取得できることが分かります。
DUALモードのAAAAレコード
最後にDual-stack modeに対応させたAuroraクラスターに対してaaaa
オプションをつけたdigコマンドを実行してみます。
[ssm-user@ip-10-0-26-43 bin]$ dig ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com aaaa
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <<>> ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com aaaa
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26356
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. IN AAAA
;; ANSWER SECTION:
ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ipv6-aurora-stack-ipv6clusterinstance1e72b1cb2-ddtthpmgkzla.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com.
ipv6-aurora-stack-ipv6clusterinstance1e72b1cb2-ddtthpmgkzla.cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com. 5 IN AAAA 2406:da14:5fc:a504:7677:2268:c785:734e
;; Query time: 2 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Sun Aug 28 13:40:22 UTC 2022
;; MSG SIZE rcvd: 248
ANSWER SECTION
にAAAAレコード及びIPv6アドレスがありますね!
Dual-stack modeによってIPv6アドレスが取得できました。
接続してみる
先ほどの調査によってDNSでAAAAレコードが返ってくることが分かりました。
では、実際に接続してIPv6アドレスが利用されるか確認してみましょう。
MySQLの場合、STATUS
でCurrent user
の値を参照すれば接続元のIPアドレスが分かるため、通常通り接続した後にこれを利用します。
[ssm-user@ip-10-0-26-43 bin]$ mysql -u admin -p -h ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws.com
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 83
Server version: 8.0.23 Source distribution
Copyright (c) 2000, 2022, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> STATUS
--------------
mysql Ver 8.0.30 for Linux on aarch64 (MySQL Community Server - GPL)
Connection id: 83
Current database:
Current user: admin@2406:da14:5fc:a500:d8ef:99cb:a50a:224c
SSL: Cipher in use is ECDHE-RSA-AES128-GCM-SHA256
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 8.0.23 Source distribution
Protocol version: 10
Connection: ipv6-aurora-stack-ipv6cluster75c8ae9d-gchhlgof1iox.cluster-cknoaqu2rllp.ap-northeast-1.rds.amazonaws via TCP/IP
Server characterset: utf8mb4
Db characterset: utf8mb4
Client characterset: utf8mb4
Conn. characterset: utf8mb4
TCP port: 3306
Binary data as: Hexadecimal
Uptime: 4 hours 56 min 13 sec
Threads: 6 Questions: 98452 Slow queries: 0 Opens: 250 Flush tables: 3 Open tables: 166 Queries per second avg: 5.539
Current user
がadmin@2406:da14:5fc:a500:d8ef:99cb:a50a:224c
になっていることからIPv6で接続されていることが分かります。
なお、IPv6のアウトバウンドトラフィックを拒否して実行するとIPv4で接続されるまでしばらく時間が必要だったことから、IPv6が優先されていることも確認できました。
さいごに
CDKでIPv6環境を構築するのは少し骨が折れました・・・。
もちろん、CDKを用いることで継続的に運用しやすい仕組みが生まれるためCDKを使用せずに構築するよりはCDKを使用して構築した方がいいと思います。しかし、いくつかのエスケープハッチを利用する必要がある点は他のリソースを作成する時よりもメリットが薄れてしまう気がしました。時代の流れに合わせてIPv6対応をどこまで早くかつ良いインターフェースで取り入れられるかによってCDKの今後に普及に影響する気がしています。
Discussion