# Create VPC with IPv6 subnets in AWS CDK

This post describes how you can create a VPC with subnets that support IPv6 with AWS CDK in Python.

At the moment of writing, the VPC L2 (layer 2) construct in AWS CDK does not support IPv6 subnets, therefore I have created the VPC from scratch using L1 constructs (aka CFN Resources).

## The code

First, I define a properties object that holds configuration.

```python
import typing

import aws_cdk as cdk
from aws_cdk import aws_ec2 as ec2
from constructs import Construct

from pydantic import BaseModel


class VpcIpv6Props(BaseModel):
    vpc_name: str
    vpc_ipv4_cidr_block: str
    number_of_azs: int
```

I define a `VpcIpv6` construct and, within it, I start by creating a VPC using the L1 constrcut.

```python
class VpcIpv6(Construct):
    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        props: VpcIpv6Props,
        **kwargs: typing.Any,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        vpc = ec2.CfnVPC(
            self,
            "vpc",
            cidr_block=props.vpc_ipv4_cidr_block,
            enable_dns_support=True,
            enable_dns_hostnames=True,
            tags=[cdk.CfnTag(key="Name", value=props.vpc_name)],
        )
        self.vpc = vpc
```

What follows, is code that is defined within `__init__()` function within `VpcIpv6`.

I associate an IPv6 CIDR range to the VPC.

```python
ec2.CfnVPCCidrBlock(self, "ipv6cidr", vpc_id=vpc.attr_vpc_id, amazon_provided_ipv6_cidr_block=True)
```

I create an Internet Gateway, and I attach it to the VPC.

```python
internet_gateway = ec2.CfnInternetGateway(
    self, "igw", tags=[cdk.CfnTag(key="Name", value=f"{props.vpc_name}-igw")]
)

ec2.CfnVPCGatewayAttachment(
    self,
    "igw-attachment",
    vpc_id=vpc.attr_vpc_id,
    internet_gateway_id=internet_gateway.attr_internet_gateway_id,
)
```

I create an Egress-Only Internet Gateway and attach it to the VPC. This is the equivalent of a NAT Gateway in IPv4, and the good news is that the Egress-Only Internet Gateway has no fixed hourly cost like NAT Gateway.

```python
egress_only_internet_gateway = ec2.CfnEgressOnlyInternetGateway(self, "egress-only-igw", vpc_id=vpc.attr_vpc_id)
```

I create a Route Table for the public subnets. I create a default route to the Internet Gateway for IPv4 and IPv6.

```python
public_subnet_route_table = ec2.CfnRouteTable(
    self,
    "public-subnet-route-table",
    vpc_id=vpc.attr_vpc_id,
    tags=[cdk.CfnTag(key="Name", value=f"{props.vpc_name}-public")],
)

ec2.CfnRoute(
    self,
    "public-subnet-default-route-ipv4",
    destination_cidr_block="0.0.0.0/0",
    route_table_id=public_subnet_route_table.attr_route_table_id,
    gateway_id=internet_gateway.attr_internet_gateway_id,
)

ec2.CfnRoute(
    self,
    "public-subnet-default-route-ipv6",
    destination_ipv6_cidr_block="::/0",
    route_table_id=public_subnet_route_table.attr_route_table_id,
    gateway_id=internet_gateway.attr_internet_gateway_id,
)
```

I create a Route Table for private subnets. I create a default route for only IPv6. The private subnet in this example is a IPv6-only subnet.

```python
private_subnet_route_table = ec2.CfnRouteTable(
    self,
    "private-subnet-route-table",
    vpc_id=vpc.attr_vpc_id,
    tags=[cdk.CfnTag(key="Name", value=f"{props.vpc_name}-private")],
)

ec2.CfnRoute(
    self,
    "private-subnet-default-route-ipv6",
    destination_ipv6_cidr_block="::/0",
    route_table_id=private_subnet_route_table.attr_route_table_id,
    egress_only_internet_gateway_id=egress_only_internet_gateway.attr_id,
)
```

I get the list of availability zones. I slice the VPC CIDR range in smaller ranges to be allocated to subnets. Note that for IPv6 subnets must be allocated a `/64` range.

```python
all_available_azs = cdk.Fn.get_azs()

vpc_ipv6_cidr_block = cdk.Fn.select(0, vpc.attr_ipv6_cidr_blocks)

ipv6_cidr_blocks = cdk.Fn.cidr(vpc_ipv6_cidr_block, 2**8, "64")
ipv4_cidr_blocks = cdk.Fn.cidr(props.vpc_ipv4_cidr_block, 2**4, "12")
```

Finally, I loop through the availability zones and I create a public and a private subnet in each. Public subnets are dual-stack, they are given both IPv4 and IPv6 CIDR blocks. Private subnets are IPv6-only. I associate each subnet with the corresponding route table.

```python
for az_index in range(props.number_of_azs):
    az_no = az_index + 1

    public_subnet = ec2.CfnSubnet(
        self,
        f"public-subnet-{az_no}",
        vpc_id=vpc.attr_vpc_id,
        cidr_block=cdk.Fn.select(2 * az_index, ipv4_cidr_blocks),
        ipv6_cidr_block=cdk.Fn.select(2 * az_index, ipv6_cidr_blocks),
        availability_zone=cdk.Fn.select(az_index, all_available_azs),
        map_public_ip_on_launch=True,
        assign_ipv6_address_on_creation=True,
        tags=[cdk.CfnTag(key="Name", value=f"{props.vpc_name}-public-{az_no}")]
    )
    ec2.CfnSubnetRouteTableAssociation(
        self,
        f"public-subnet-{az_no}-route-table-association",
        route_table_id=public_subnet_route_table.attr_route_table_id,
        subnet_id=public_subnet.attr_subnet_id,
    )

    private_subnet = ec2.CfnSubnet(
        self,
        f"private-subnet-{az_no}",
        vpc_id=vpc.attr_vpc_id,
        ipv6_cidr_block=cdk.Fn.select(2 * az_index + 1, ipv6_cidr_blocks),
        availability_zone=cdk.Fn.select(az_index, all_available_azs),
        ipv6_native=True,
        tags=[cdk.CfnTag(key="Name", value=f"{props.vpc_name}-private-{az_no}")]
    )
    ec2.CfnSubnetRouteTableAssociation(
        self,
        f"private-subnet-{az_no}-route-table-association",
        route_table_id=private_subnet_route_table.attr_route_table_id,
        subnet_id=private_subnet.attr_subnet_id,
    )
```

This is how you instantiate the construct

```python
VpcIpv6(
    self,
    "vpc",
    VpcIpv6Props(
        vpc_name="my-ipv6-vpc",
        vpc_ipv4_cidr_block="10.0.0.0/16",
        number_of_azs=3,
    ),
)
```

## Connectivity tests

After you have included the above construct in your CDK project and have deployed it, it is time to test IPv6 connectivity from public and private subnets.

I create 2 EC2 instances of type `t3` (you need the latest generation), one in the public subnet (with a public IP address for both IPv4 and IPv6) and one in the private subnet. I am using the same key pair for both instances.

First I add the SSH key to the authentication agent

```bash
ssh-add ~/path/to/your/key.pem
```

Then I remote to the public EC2 instance (make sure the Security Group allows SSH access). I do not have IPv6 connectivity at home, that's why I am using the IPv4 public IP address to access the public instance. The `-A` is important so that you can jump from the public instance onto the private instance.

```bash
ssh -A ec2-user@<public IPv4 address of public instance>
```

As a first test, I check that the public EC2 instance can access Google over IPv6.

```bash
wget http://ipv6.google.com
```

Then I copy the IPv6 address of the private EC2 instance, and on the same terminal, I jump from the public EC2 instance to the private EC2 instance via IPv6.

```bash
ssh ec2-user@<IPv6 address of private instance>
```

I repeat the test

```bash
wget http://ipv6.google.com
```

Congratulations! You have created public and private subnets with IPv6 support.
