AWS VPC IPAM Tutorial – The Better Terraform Way (7 Steps Faster)

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!

1 comment

  1. 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?

Leave a comment

Your email address will not be published. Required fields are marked *