AWS VPC IPAM Tutorial - The Better Terraform Way (7 Steps Faster)
Published: 2022-01-10 14:06:01

In my previous post covering the basics of [AWS VPC IPAM](https://dev.to/aws-builders/aws-vpc-ipam-basics-and-why-you-need-to-be-careful-5abn), I promised some Terraform code samples. 

My Terraform code sample will be based on the [AWS VPC IPAM tutorial from the official documentation](https://docs.aws.amazon.com/vpc/latest/ipam/tutorials-create-vpc-ipam.html). 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](https://www.terraform.io/language/values/variables#variable-definitions-tfvars-files). Otherwise, the default values will work.

```hcl
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
```hcl
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`.
```hcl
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](https://www.terraform.io/language/functions/toset).
```hcl
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](https://www.terraform.io/language/expressions/dynamic-blocks).

Another interesting point is the [depends_on meta-argument](https://www.terraform.io/language/meta-arguments/depends_on) 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](https://docs.aws.amazon.com/vpc/latest/ipam/iam-ipam-slr.html). 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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_ipam#attributes-reference). To understand more about what a scope is, do check out [my previous post](https://dev.to/aws-builders/aws-vpc-ipam-basics-and-why-you-need-to-be-careful-5abn) covering the basics of AWS VPC IPAM.
```hcl
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](https://www.terraform.io/language/meta-arguments/for_each), the resource taps on `local.deduplicated_region_list` local variable again to create multiple regional pools according to the region list you've set. 
```hcl
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.
```yaml
{ 
  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](https://www.terraform.io/language/functions/cidrsubnet). 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. 
```hcl
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!