Using Terraform to provision IPv6 via an IPAM


title: Using Terraform to provision IPv6 via an IPAM

date: 2024/10/03 09:00:25


I do a lot of reading for my work, often I know what I need to achieve and usually someone has done it before and I can find it and adapt it for my own needs.

But for some reason this time I couldn’t find anything on this subject. Weird! In this whole of the internets?!

So anyways, what was I trying to do?

We have all our accounts in an AWS Organisation. Someone wants an account, we create it, they get access via Azure AD groups. Some people know what they are doing and get a blank account, some people want the account managed for them - in this case we scaffold resources using a Terraform module created in-house.

It sets up the VPC, the subnets, all that jazz. But here’s the thing - it’s IPv4 only, and it’s 2024 now… This won’t do.

Of course, IPv4 is fine for most things for us. But sometimes we want private traffic to route between accounts, so each account gets a transit gateway attachment. We use IP address management on a network AWS account to allocate private IPv4 address space, it’s nice - we don’t then get overlaps and it makes things a little simpler.

Now you’re thinking well, no problem with IPv6 right? Just get AWS to allocate from their range and yes you’re right - that’s easy. But you do get some nice things with an IPAM and down the line for future me I’m thinking about some of the requests I get - where the hell is this resource? What account is it in? With IPv4 I know what ranges are allocated to which account, so it’d be nice to have the same for IPv6.


But all the writeups online only talk about using the AWS allocated IPv6 ranges, IPAM allocation is in the terraform documentation yes - but there’s no examples.


   resource "aws_vpc" "vpc" {
     cidr_block          = var.ipam_enable ? null : var.vpc_cidr
     ipv4_ipam_pool_id   = var.ipam_enable ? var.ipam_pool_id : null
     ipv4_netmask_length = var.ipam_enable ? var.ipam_netmask : null
     ipv6_ipam_pool_id   = var.ipv6_enable ? var.ipv6_ipam_pool_id : null
     ipv6_netmask_length = var.ipv6_enable ? 60 : null
     instance_tenancy = "default"
     tags = {
     Name = "VPC Name"
     }
  }

So we’ve got a bool that says we want IPv6 - ipv6_enable. If so set the ipv6_ipam_pool_id. The tricky bit with IPv6 in AWS IPAM is the netmask, IPAM has gotten us a pool of a /52, and each allocation to a VPC has to be a /60.

Using this we can create our subnets. We’ve a variable that says how many private and public one’s we want so now we just have to adjust the cidrsubnet function to divvy up the IPAM allocated block.

resource "aws_subnet" "private_subnet" {
 count             = var.enabled_availability_zones
 vpc_id            = aws_vpc.vpc.id
 cidr_block        = cidrsubnet(aws_vpc.vpc.cidr_block, var.max_availability_zones, count.index)
 ipv6_cidr_block   = var.ipv6_enable ? cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 4, count.index) : null
 availability_zone = data.aws_availability_zones.available.names[count.index]
 tags = {
 Name = "Private Subnet ${count.index}"
 }
}
resource "aws_subnet" "public_subnet" {
 count                           = var.enabled_availability_zones
 vpc_id                          = aws_vpc.vpc.id
 cidr_block                      = cidrsubnet(aws_vpc.vpc.cidr_block, var.max_availability_zones, var.max_availability_zones + count.index)
 ipv6_cidr_block                 = var.ipv6_enable ? cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 4, var.max_availability_zones + count.index) : null
 assign_ipv6_address_on_creation = var.ipv6_enable ? true : false
 availability_zone               = data.aws_availability_zones.available.names[count.index]
 tags = {
 Name = "Public Subnet ${count.index}"
 }
}

IPv6 subnets have to be

api error InvalidSubnet.Range: The CIDR ‘aaaa:bbbb:cccc:dddd:eeee::/68’ must be one of /44, /48, /52, /56, /60, /64

So I used terraform console to check my function to make sure I was going to get one of those out. You might have to do something different here depending on the size of netblock you’ve set the IPAM to hand out.

The usual blocks to set up your routing, and voila - your VPC will have IPv6 in it now, ready to allocate to ec2’s, load balancers, whatever.

Note: I did have to use terraform apply -target=module.your-vpc-module.module.vpc.aws_vpc.vpc to make sure the VPC has the IPv6 allocation first, before it was trying to allocate to the subnets. I suspect this was because my VPC was already created.