In my previous post covering the basics of AWS VPC IPAM, I promised some Terraform code samples.
My Terraform code sample will be based on the AWS VPC IPAM tutorial from the official documentation. As you can already see from the title, the “Terraform way” is 7 steps shorter than the official tutorial and undoubtedly better.
The Better Terraform Way
Step 1: Clone Github repository
From your terminal, run git clone https://github.com/sgLancelot/aws-vpc-ipam-terraform-tutorial.git
. Change your current directory to the cloned code with cd aws-vpc-ipam-terraform-tutorial
.
Step 2: Terraform Apply
Note: This step assumes you already have your AWS credentials set up and Terraform installed. As of this point of writing, you will require Terraform version 1.1.2 or higher.
From your terminal, run terraform apply
. Review the Terraform planned changes before you type yes
. If you are using the default values, you should expect a plan consisting of 9 to add, 0 to change, 0 to destroy
.
After applying the changes, head over to your AWS console to view what you’ve created.
Step 3: Terraform Destroy
Note: This step may take up to 25 minutes to complete. From testing, this seems to be caused by the pool CIDR assignment requiring some time to detect that the test VPC is deleted before allowing you to unassign the CIDR.
From your terminal, run terraform destroy
. Review the Terraform planned changes before you type yes
. If you are using the default values, you should expect a plan consisting of 0 to add, 0 to change, 9 to destroy
.
That concludes the AWS VPC IPAM tutorial, the better Terraform way. Next, we will walkthrough the code to give you an understanding of what you just created and destroyed.
Bonus: Code Walkthrough
In this section, I’ll walkthrough my code and describe my thought processes.
Variables
In the variables.tf
file, you can see where the variables are defined. The default values follows the tutorial, but I’ve put them as variables to give you the option to define them yourselves in your .tfvars
file, if you choose to do so. Otherwise, the default values will work.
variable "region" { type = string description = "The AWS Region that the resources will be created in. Will also be included as part of the IPAM operating region" default = "us-east-1" } variable "ipam_operating_regions" { type = list(string) description = "Additional AWS VPC IPAM operating regions. You can only create VPCs from a pool whose locale matches this variable. Duplicate values will be removed." default = ["us-west-2"] } variable "top_level_pool_cidr" { type = string description = "The top level IPAM pool CIDR. Currently only supports a single CIDR." default = "10.0.0.0/8" }
The variables are pretty self-explanatory from the description I’ve added.
From here on, all the code can be found in main.tf
.
Service-Linked Role
resource "aws_iam_service_linked_role" "ipam" { aws_service_name = "ipam.amazonaws.com" description = "Service Linked Role for AWS VPC IP Address Manager" }
I’ve chosen to follow the tutorial without using AWS Organizations, and hence, the service-linked IAM role needs to be created for VPC IPAM to automatically discover resources to monitor. There is nothing fancy here.
IPAM and it’s operating regions
The IPAM construct requires you to define its operating region. One of the operating region must include the AWS provider block region, in this case, it’s defined as var.region
.
locals { deduplicated_region_list = toset(concat([var.region], var.ipam_operating_regions)) }
The deduplicated_region_list
local variable ensures that the list of regions that you pass into IPAM does not have any duplication, which might cause an error when creating the IPAM. To learn more about the toset function in the Terraform documentation.
resource "aws_vpc_ipam" "tutorial" { description = "my-ipam" dynamic "operating_regions" { for_each = local.deduplicated_region_list content { region_name = operating_regions.value } } depends_on = [ aws_iam_service_linked_role.ipam ] }
Here, local.deduplicated_region_list
is passed into the operating systems configuration block as a dynamic block.
Another interesting point is the depends_on meta-argument to create a dependency between the service-linked role and IPAM. This allows the IPAM to be deleted before the service-link role is deleted. From testing, letting Terraform perform this deletion without depends_on
actually causes an error as it deletes the service-linked role and IPAM in parallel.
Top-level Pool and CIDR assignment
Along with the creation of the IPAM, a default private and public scope is created as well and can be reference via the IPAM Terraform resource’s attributes. To understand more about what a scope is, do check out my previous post covering the basics of AWS VPC IPAM.
resource "aws_vpc_ipam_pool" "top_level" { description = "top-level-pool" address_family = "ipv4" ipam_scope_id = aws_vpc_ipam.tutorial.private_default_scope_id } # provision CIDR to the top-level pool resource "aws_vpc_ipam_pool_cidr" "top_level" { ipam_pool_id = aws_vpc_ipam_pool.top_level.id cidr = var.top_level_pool_cidr # "10.0.0.0/8" if following the tutorial }
The top-level pool will be created in the IPAM’s private scope. The var.top_level_pool_cidr
variable follows the tutorial with 10.0.0.0/8
. You may set a different CIDR as long as it as a netmask of above/16
.
Sub-level Pool and CIDR assignment
For this part, I further improved on the tutorial of simply creating just 1 regional sub-level pool. Using the for_each meta-argument, the resource taps on local.deduplicated_region_list
local variable again to create multiple regional pools according to the region list you’ve set.
resource "aws_vpc_ipam_pool" "regional" { for_each = local.deduplicated_region_list description = "${each.key}-pool" address_family = "ipv4" ipam_scope_id = aws_vpc_ipam.tutorial.private_default_scope_id locale = each.key source_ipam_pool_id = aws_vpc_ipam_pool.top_level.id } resource "aws_vpc_ipam_pool_cidr" "regional" { for_each = { for index, region in tolist(local.deduplicated_region_list) : region => index } ipam_pool_id = aws_vpc_ipam_pool.regional[each.key].id cidr = cidrsubnet(var.top_level_pool_cidr, 8, each.value) }
For the CIDR assignment for these regional pools, I had to convert the toset-transformed local.deduplicated_region_list
to a list again. The purpose was to allow the for
expression to properly retrieve the index of each region.
The code snippet below should help you visualize how the for
expression looks like for the default values.
{ us-east-1 = 0, us-west-2 = 1 }
With that, each.key
would iterate through the key (the regions) and each.value
would be the corresponding index in the list.
The reason why we needed the index value is to generate the sub-level pool CIDR dynamically from the top-level pool CIDR var.top_level_pool_cidr
using the cidrsubnet
Terraform function. In short, the cidrsubnet
function slices up a CIDR according to the values you pass into it.
VPC to test
Finally, we’ve reached the end of the code sample. We will create a test VPC in the AWS provider region defined in var.region
.
In the tutorial, they’ve manually assigned a direct CIDR block to this VPC, which seems ludicrous because it doesn’t showcase the strength of an IPAM-managed VPC.
resource "aws_vpc" "tutorial" { ipv4_ipam_pool_id = aws_vpc_ipam_pool.regional[var.region].id ipv4_netmask_length = 24 depends_on = [ aws_vpc_ipam_pool_cidr.regional ] }
In my Terraform code, we’ve chosen to test the new attributes added in AWS provider version 3.68.0
, which for as long as I can remember, removes the cidr_block
attribute requirement of being mandatory; cidr_block
is now optional if you have the ipv4_ipam_pool_id
and ipv4_netmask_length
attributes. For my example, I’ve requested a CIDR of /24 netmask length from the regional sub-level pool.
It should achieve the same results as the tutorial as long as you are using the default values.
Closing
I had a blast writing this short Terraform code sample for AWS VPC IPAM. If there is anything you feel I could’ve done better, please reach out to me.
Hope you have learnt something!
Great post! I am currently building this out in Terraform but am wondering how to include subnets in your configuration when you don’t know what the CIDR allocation will be yet? If you have set the IPAM pool for your VPC, how do you then create subnets through terraform without a CIDR?