Skip to main content

Manage Azure IPAM with Terraform

·1561 words·8 mins
Azure Mvp Terraform Networking

Managing IP addresses across teams, environments, subscriptions, and even Azure tenants, can be complex.

The Azure IPAM service simplifies this work a bit. You create a pool of IP addresses that are available for your teams. To consume addresses from the pool the teams just point at the IPAM pool and ask for a number of addresses (e.g. 200). No need to know more details about what ranges you can use or how many addresses a /23 or /25 network is.

In this blog post I will go through how to provision and use the Azure IPAM service with Terraform.

What is IPAM?
#

IPAM, or IP Address Management, is a service offered by Azure to manage distributing IP address blocks from a pool of available IP addresses. This is not an Azure specific concept, there are many tools available in the IPAM space.

However, the Azure IPAM solution was recently announced in general availability in multiple Azure regions.

The best part of an IPAM is that you remove the need for the consumers of IP addresses to know anything more than how many addresses that they need. You don’t need to communicate what specific range of IP addresses they have available to select from. The team simply needs to provide the number, e.g. 42, of addresses.

This might seem like a small thing, but in my eyes it will solve a lot of confusion and administrative work for platform teams and cloud architects.


Below we will see how an IPAM resource is provisioned on Azure using Terraform. We will first be the producers (a platform team) that provisions the IPAMs and related resources. Next we will take on the role of the consumers (a development team) that will provision virtual networks that consumes IP addresses from the IPAM.

Provision a Network Manager and IPAM Pools
#

Imagine that we are part of the platform team in our organization. We have been tasked with creating an IPAM pool for our Swedish branch. We will divide the pool into smaller chunks for different sections of the Swedish branch.

We begin our Terraform configuration by specifying that we will use the AzureRM provider:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"

      # you pin your provider versions, right?
      version = "4.35.0"
    }
  }
}

provider "azurerm" {
  features {}

  # Configured via ARM_SUBSCRIPTION_ID
  # subscription_id = ""
}

We could make the Terraform configuration reusable and add a few variables for the location as well as the base CIDR block of the IPAM pool. However, for now we are satisfied with adding local values for these:

locals {
  location = "swedencentral"
  cidr     = "10.0.0.0/16"
}

An IPAM pool is part of a service called Network Manager. This service is a collection of different network related services, where IPAM is one.

We configure a network manager resource where our IPAM pools will live:

data "azurerm_subscription" "current" {}

resource "azurerm_network_manager" "default" {
  name                = "network-manager-${local.location}"
  location            = azurerm_resource_group.default.location
  resource_group_name = azurerm_resource_group.default.name

  scope {
    subscription_ids = [data.azurerm_subscription.current.id]
  }
}

The network manager is available in a scope. In this example the scope is set to a subscription, but you could also set it at a management group scope.

Now we can add our main IPAM:

resource "azurerm_network_manager_ipam_pool" "main" {
  name               = "ipam-${local.location}"
  location           = local.location
  network_manager_id = azurerm_network_manager.default.id
  display_name       = "ipam-${local.location}"
  address_prefixes   = [local.cidr]
}

The IPAM pool can be configured with multiple address prefixes, both IPv4 and IPv6. In this case we only have a single IPv4 prefix.

As mentioned, we want to split the main IPAM into child pools. You can do this by configuring additional IPAM resources and using the parent_pool_name argument:

resource "azurerm_network_manager_ipam_pool" "west" {
  name               = "ipam-${local.location}-west"
  location           = local.location
  network_manager_id = azurerm_network_manager.default.id
  display_name       = "ipam-${local.location}-west"
  parent_pool_name   = azurerm_network_manager_ipam_pool.main.name
  address_prefixes   = [cidrsubnet(local.cidr, 2, 0)]
}

resource "azurerm_network_manager_ipam_pool" "east" {
  name               = "ipam-${local.location}-east"
  location           = local.location
  network_manager_id = azurerm_network_manager.default.id
  display_name       = "ipam-${local.location}-east"
  parent_pool_name   = azurerm_network_manager_ipam_pool.main.name
  address_prefixes   = [cidrsubnet(local.cidr, 2, 1)]
}

resource "azurerm_network_manager_ipam_pool" "north" {
  name               = "ipam-${local.location}-north"
  location           = local.location
  network_manager_id = azurerm_network_manager.default.id
  display_name       = "ipam-${local.location}-north"
  parent_pool_name   = azurerm_network_manager_ipam_pool.main.name
  address_prefixes   = [cidrsubnet(local.cidr, 2, 2)]
}

resource "azurerm_network_manager_ipam_pool" "south" {
  name               = "ipam-${local.location}-south"
  location           = local.location
  network_manager_id = azurerm_network_manager.default.id
  display_name       = "ipam-${local.location}-south"
  parent_pool_name   = azurerm_network_manager_ipam_pool.main.name
  address_prefixes   = [cidrsubnet(local.cidr, 2, 3)]
}

An important note is that you still need to configure a valid address prefix for a child pool. I think it would have been nice if we could have “consumed” a number of IP addresses from the parent IPAM in a similar way that we will consume IP addresses when we create virtual networks later. Oh well!

We have four child pools that each consumes a fourth of the total available IP addresses.

NameIP prefixNumber of addresses
ipam-swedencentral-west10.0.0.0/1816384
ipam-swedencentral-east10.0.64.0/1816384
ipam-swedencentral-north10.0.128.0/1816384
ipam-swedencentral-south10.0.192.0/1816384

Add an output for the IPAM resource IDs to simplify finding these values:

output "ipam_pool_ids" {
  value = {
    north = azurerm_network_manager_ipam_pool.north.id
    south = azurerm_network_manager_ipam_pool.south.id
    west  = azurerm_network_manager_ipam_pool.west.id
    east  = azurerm_network_manager_ipam_pool.east.id
  }
}

Consume IPAM Pools
#

Now we step into the shoes of the consumer team. The consumers consists of a team of developers based in the west part of Sweden, in Gothenburg to be exact.

We need to create an Azure virtual network for our application. This is an example of how we used to do it before:

resource "azurerm_virtual_network" "app" {
  name                = "vnet-app"
  resource_group_name = azurerm_resource_group.default.name
  location            = var.location
  address_space       = ["10.0.0.0/24"]
}

This required that we got the IP address range handed to us in some way, perhaps distributed via email from the platform team.

However, we have heard that the platform team has finally set up IPAM for us to use!

Instead of configuring the address_space argument for the virtual network we can use an ip_address_pool block:

resource "azurerm_virtual_network" "app" {
  name                = "vnet-app"
  resource_group_name = azurerm_resource_group.default.name
  location            = var.location

  ip_address_pool {
    id                     = "<id>"
    number_of_ip_addresses = 256
  }
}

The ip_address_pool block takes the resource ID of the pool we want to use, and the number of IP addresses we need. Note that if we attempt to use the address_space argument at the same time as an ip_address_pool we will get an error:

$ terraform plan

│ Error: Invalid combination of arguments
│   with azurerm_virtual_network.app,
│   on main.tf line 26, in resource "azurerm_virtual_network" "app":
│   26: resource "azurerm_virtual_network" "app" {
│ "ip_address_pool": only one of `address_space,ip_address_pool` can be specified, but `address_space,ip_address_pool`
│ were specified.

The number_of_ip_addresses argument in the ip_address_pool block technically accepts a string value. However, we can rely on the implicit conversion from a number to a string.

The only tricky part is that we need the resource ID of the IPAM resource. This ID has the format:

/subscriptions/<id>/resourceGroups/<name>/providers/Microsoft.Network/networkManagers/<name>/ipamPools/<name>

There is currently no corresponding data source for the IPAM pool resource type.


That was the whole scenario! The producers are happy that they can monitor the IP usage from a single pane of glass in the Azure portaL:

Overview of the different IPAMs and their utilization

FAQ
#

Can you configure multiple IP address pools for a single virtual network?
#

You can, but only one IPv4 and one IPv6. It is not possible to configure multiple address pools of the same type of IP version.

Can you allocate any number of IP addresses from the available pool?
#

No. The closest available prefix (greater than what you requested) will be allocated for you.

For instance: if the pool has 16,384 IP addresses available (corresponding to a /18 network) and you request 5,000 addresses - then you will get 8,192 IP addresses (corresponding to a /19 network). The IPAM will not start splitting up the ranges to give you exactly 5,000 addresses.

What happens if there is no more IP addresses available in the pool?
#

You get a handy error message informing you that it is not possible to reserve a large enough block from the pool:

$ terraform apply -auto-approve 
...
...
│ Error: creating Virtual Network (Subscription: "<id>"
│ Resource Group Name: "rg-networking"
│ Virtual Network Name: "vnet-shared-north"): performing CreateOrUpdate: unexpected status 400
│ (400 Bad Request) with error: IpamApiCallFailed: Ipam API call failed with status BadRequest
│ with error message Not able to find available prefixes with the specified number of IPs, while
│ also following the Engine policy of maximum 2 prefixes allowed, and respecting the prefix ranges
│ allowed for the VNet., please check the parameters and retry.
│   with azurerm_virtual_network.shared_north,
│   on main.tf line 70, in resource "azurerm_virtual_network" "shared_north":
│   70: resource "azurerm_virtual_network" "shared_north" {

Why is there no data source for the IP address pool resource?
#

This is a good question! The resource is there, but there is no corresponding data source as of version 4.35.0 of the AzureRM provider.

Can you consume the IPAM pool from different regions?
#

No. You can only consume an IPAM in the same region as where you want to use it. I assume this is in the documentation, but I discovered it when I tried to consume an IPAM cross-region.

Conclusions
#

I am a fan of IPAM! I believe it is easy to configure and simplifies a lot when it comes to IP address management.

Azure IPAM is generally available and you can start using it for your production workloads today.

The only thing I miss is a data source for IPAM in the Terraform provider …

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect · Author · HashiCorp Ambassador · Microsoft MVP