パブリックIPv4アドレスを割り当てずIPv6アドレスを割り当てたEC2インスタンスが外部と通信するために必要なリソースをCDKで作成する

aws

まもなくEIP以外のパブリックIPv4アドレスにも課金されるようになるためIPv6への移行を進めていきたい今日この頃、CDK で IPv6 環境を構築し、外部と通信する際に必要なリソースや挙動を確認してみた。

VPCの作成

VPCに割り当てられるIPv6のブロックは /56 固定で、サブネットは /64 固定となっている。パブリックIPv4アドレスは自動で割り当てられないように、IPv6アドレスは自動で割り当てられるようにした。

const vpc = new ec2.Vpc(this, 'VPC', {
  ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/22'),
  maxAzs: 2,
  natGateways: 0,
  subnetConfiguration: [
    {
      subnetType: ec2.SubnetType.PUBLIC,
      name: 'Public',
      cidrMask: 24,
      mapPublicIpOnLaunch: false  // settings for Auto-assign public IPv4 address (default: true)
    },
    {
      subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      name: 'Private',
      cidrMask: 24,
    }
  ],
});

const cidrBlockIPV6 = new ec2.CfnVPCCidrBlock(this, 'CidrBlockIPV6', {
  vpcId: vpc.vpcId,
  amazonProvidedIpv6CidrBlock: true,
})

const vpcIpv6CidrBlock = cdk.Fn.select(0, vpc.vpcIpv6CidrBlocks);

// IPV6 ::/56 block is split into 256 chunks of ::/64 blocks
// e.g. 2406:da14:856:3200::/64, 2406:da14:856:3201::/64, ...
const subnetIpv6CidrBlocks = cdk.Fn.cidr(vpcIpv6CidrBlock, 256, "64");

[...vpc.publicSubnets, ...vpc.privateSubnets].forEach((subnet, i) => {
  subnet.node.addDependency(cidrBlockIPV6);
  const cfnSubnet = subnet.node.defaultChild as ec2.CfnSubnet
  cfnSubnet.ipv6CidrBlock = cdk.Fn.select(i, subnetIpv6CidrBlocks);
  cfnSubnet.assignIpv6AddressOnCreation = true
  cfnSubnet.enableDns64 = true
})

ルートテーブルの更新

パブリックサブネットは v4 と同じくigwに向ければ良いが、プライベートサブネットは NAT せず Egress-only インターネットゲートウェイに向ける。 Egress-only インターネットゲートウェイは NAT ゲートウェイと異なり無料で利用できるが、v6 にしか対応していないので v4 での通信は従来通り NAT ゲートウェイを用いる必要がある。 SSM はじめ IPv6 に対応していないサービスの API を呼ぶ際に v4 での通信が必要になる。

また、パブリックIPv4アドレスが割り当てられていないパブリックサブネットのインスタンスからも v4 で通信できるように DNS64 を有効にして NAT64 のためのルートを追加している。 DNS64 は IPv4 アドレスを RFC6052 の 64:ff9b:: prefix を付けることで IPv6 として返し、 この prefix 付きのアドレスを NAT ゲートウェイが受け取ると IPv4 パケットへの変換が行われるのでパブリックIPv4アドレスなしで通信することができる。

const natGatewayEIP = new ec2.CfnEIP(this, 'NatGatewayEIP', {
  domain: 'vpc',
})
const natGateway = new ec2.CfnNatGateway(this, 'NatGateway', {
  allocationId: natGatewayEIP.attrAllocationId,
  subnetId: vpc.publicSubnets[0].subnetId,
})

vpc.publicSubnets.forEach((subnet) => {
  const ec2Subnet = subnet as ec2.Subnet

  ec2Subnet.addRoute('DefaultIPV6Route', {
    routerType: ec2.RouterType.GATEWAY,
    routerId: vpc.internetGatewayId!,
    destinationIpv6CidrBlock: "::/0",
    enablesInternetConnectivity: true,
  })

  ec2Subnet.addRoute('NAT64IPV6Route', {
    routerType: ec2.RouterType.NAT_GATEWAY,
    routerId: natGateway.ref,
    destinationIpv6CidrBlock: "64:ff9b::/96",
    enablesInternetConnectivity: true,
  })
})

const egressOnlyIgw = new ec2.CfnEgressOnlyInternetGateway(this, "EgressOnlyIGW", {
  vpcId: vpc.vpcId
});

vpc.privateSubnets.forEach((subnet, i) => {
  const ec2Subnet = subnet as ec2.Subnet

  ec2Subnet.addDefaultNatRoute(natGateway.ref)

  ec2Subnet.addRoute('DefaultIPV6Route', {
    routerType: ec2.RouterType.EGRESS_ONLY_INTERNET_GATEWAY,
    routerId: egressOnlyIgw.ref,
    destinationIpv6CidrBlock: "::/0",
    enablesInternetConnectivity: true,
  })
})

挙動確認

パブリックとプライベートサブネットにインスタンスを立てた。IPv6 の Outbound を許可している。

const role = new iam.Role(this, 'EC2Role', {
  assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
  ],
});

const securityGroup = new ec2.SecurityGroup(this, 'EC2SecurityGroup', {
  vpc,
  allowAllOutbound: true,
  allowAllIpv6Outbound: true,
})

const publicSubnetEC2Instance = new ec2.Instance(this, 'EC2PublicSubnet', {
  vpc,
  instanceType: new ec2.InstanceType('t2.micro'),
  machineImage: new ec2.AmazonLinuxImage(),
  role: role,
  securityGroup: securityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
})

const privateSubnetEC2Instance = new ec2.Instance(this, 'EC2PrivateSubnet', {
  vpc,
  instanceType: new ec2.InstanceType('t2.micro'),
  machineImage: new ec2.AmazonLinuxImage(),
  role: role,
  securityGroup: securityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
  },
})

インスタンスに入って AAAA レコードを返す ifconfig.io にリクエストするとインスタンスの IPv6 アドレスが返ってくることから IPv6 で通信できていることがわかる。 また、本来 AAAA レコードが返らない httpbin.org で DNS64 によって 64:ff9b:: prefix の IPv6アドレスへの名前解決が行われることと、NATゲートウェイの IPv4 アドレスが返ることから NAT64 による変換が行われていることも確認できる。


$ curl ifconfig.io
2406:da14:45a:3402:7815:32a9:72db:d9e4

$ nslookup -q=AAAA httpbin.org
Server:         10.0.0.2
Address:        10.0.0.2#53

Non-authoritative answer:
    httpbin.org     has AAAA address 64:ff9b::3448:ee09
    httpbin.org     has AAAA address 64:ff9b::3e4:853a

$ man curl
...
-6, --ipv6
This option tells curl to resolve names to IPv6 addresses only, and not for example try IPv4.

$ curl --ipv6 httpbin.org/ip
# public subnet
{
  "origin": "57.180.96.138"
}

参考

How to Create an IPV6 VPC using AWS-CDK - TurboGeek