Create resources required for EC2 instance without public IPv4 but IPv6 address to communicate with the Internet

aws

Public IPv4 addresses will start being charged like EIPs soon, so I would like to migrate them to IPv6 recently. In this article, I construct an IPv6 environment with CDK and check the resources required to communicate with the Internet and the behavior.

(PS: 2024-01-29) In v2.122.0, ipProtocol: ec2.IpProtocol.DUAL_STACK was added to ec2.Vpc, which creates resources for IPv6 automatically.

Create a VPC

IPv6 block assigned to VPC is fixed length /56 and one assigned to subnets is also fixed length /64. I disabled automatically assigning public IPv4 addresses and make IPv6 addresses assigned automatically.

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
})

Update route tables

For public subnets, add a route to the Internet gateway same as v4, but for private subnets, add a route to the Egress-only Internet gateway instead without NAT. Egress-only Internet gateway is free unlike NAT gateway, but it only supports v6, so v4 communication still requires NAT gateway. When calling APIs of services that do not support IPv6 such as SSM, v4 communication is still required.

Additionally, I enabled DNS64 and added a route for NAT64 to enable instances with only IPv6 addresses to communicate with IPv4. DNS64 returns IPv4 addresses as IPv6 ones by attaching RFC6052 64:ff9b:: prefix, and when NAT gateway receives addresses with this prefix, it converts the IPv6 packets to IPv4 ones, so instances can communicate without public IPv4 addresses. However, note that if you enable this setting in an IPv4/v6 dual-stack environment, IPv6 is preferred and so unnecessary NAT will occur and IPv4 address routing won’t work.

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,
  })
})

Check behavior

Launch instances in public and private subnets with SG accepting IPv6 outbound traffic. If you enable IPv6 in an existing subnet, you should also check the settings of Network ACLs as well as SG.

See traffic denied by SG or Network ACL with VPC Flow Logs and CloudWatch Logs Insights - sambaiz-net

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,
  },
})

Requesting to ifconfig.io, which returns AAAA records from instances, the IPv6 addresses of the instances are returned, so it can be seen that communication with IPv6 is successful. Also, httpbin.org, which does not return AAAA records, returns IPv6 addresses with 64:ff9b:: prefix by DNS64, and IPv4 addresses of NAT gateway are returned, so it can be seen that NAT64 conversion is successful.

$ 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"
}

References

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