Terraform: Get Next Subnet

3 minute read

Description:

So let’s say you are building a subnet module and you want it to dynamically calculate the next available CIDR Range to fill in the required address_range parameter, how would you do this? Here is a post on a possible solution.

1
2
3
4
5
6
resource "azurerm_subnet" "example" {
  name                 = "example-subnet"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.1.0/24"] # <== Where do we get this?
}

To Resolve

  1. So one possible solution is to have a data block that queries some kind of web app that can dynamically get the next CIDR range for a given subnet. Thankfully, this has already been set up (also see associated blog post ).

  2. After deploying the Function App above, the next step is to give the app Reader role to all subscriptions that you want it to be able to query Vnets for, probably the whole tenant.

  3. So in your subnet module, you can make a data block call like:

    1
    2
    3
    
    data "external" "json" {
    program = ["sh", "-c", "curl -X GET -v -L -H 'Connection: close' 'https://?subscriptionId=&resourceGroupName=&virtualNetworkName=&cidr=' "]
    }
    
    • Then in your subnet resource:
    1
    2
    3
    4
    5
    6
    
    resource "azurerm_subnet" "example" {
    name                 = "example-subnet"
    resource_group_name  = azurerm_resource_group.example.name
    virtual_network_name = azurerm_virtual_network.example.name
    address_prefixes     = [data.external.json.result["proposedCIDR"]]
    }
    
  4. But how to you handle when multiple calls to the subnet are made at the same time since Terraform evaluates all at once? For this one of my coworkers modified the function to have a previous_address parameter that you could pass the Function App and it would use that as a basis before generating a new subnet. Then when you called our custom subnet module you had to pass that argument as another CIDR block.

    • NOTE: Remember that a parameter is a function definition and an argument is the value passed to it. MSDN references like a parameter is a ‘P’arking space and an argument is an ‘A’utomobile. Many different automobiles can fit into a single parking space, i.e. different arguments can be passed to a single defined parameter.
  5. A key point about the subnet module that you will have to remember is to add the lifecycle block to ignore address prefixes because the data block will run every time you evaulate a subnet and we only needed it that first run. This can be done by adding a lifecycle block in the module itself like so:

    1
    2
    3
    4
    5
    
    lifecycle {
       ignore_changes = [
          address_prefixes
       ]
    }
    
  6. Another limitation is this Function App will not work if you create a blank VNET with no default subnets like through Terraform. Not sure why the Azure Rest API even allows you to create a VNET with no subnets in the first place. But the fix is to manually in the UI go and create a default subnet like a /24 and then call the Function App.

  7. Honestly, I’m a powershell/python guy myself so I would just rewrite this Function App using python with http trigger (also see my other examples ) to handle all this logic but I’m pretty busy these days :)

    • The requirments for this web app would be:
    • Required inputs: subscription_id, vnet_resource_group, vnet_name, desired_cidr
    • Optional inputs: previous_cidr
    • Logic:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    If `previous_cidr` specified:
       Check if a default subnet exists (dont trust the user who called the module that they know what they are talking about )
          if true, continue
          if false, create it
    
       Next, Query the vnet for the next `desired_cidr` block after it
    
    Else:
       Check if a default subnet exists (dont trust the user who called the module that they know what they are talking about )
          if true, continue
          if false, create it
    
       Next, give me the next `desired_cidr` block available
    
  8. Update: Here is a link to this in powershell. Feel free to send a pull request for improvements as it’s currently really messy but has been working so far in my testing.

Comments