Thursday, July 16, 2020

Terraform Subnet Splitting

How to dynamically split subnets in Terraform

Using cidrsubnets: 

The one without "s" returns a single subnet whereas the other returns a set of subnets. The "newbits" are used to determine how many additional bits of netmask to use in creating subsequent subnets. 
For example: 10.1.0.0/16. First two octets are not important. To simplify explanation, I'm going to use binary notation of last 2 octets. Here we're asking for 2 subnets with each having one more netmask. 
 
 
output "example"{
    value = cidrsubnets("10.1.0.0/16",1,1)
}

Result:
example = [
  "10.1.0.0/17",
  "10.1.128.0/17",
]

This is allowed because we can have 2 additional subnets with that netmask
  0.0 to 127.255    <- possible range
  00000000.00000000 <- network
  10000000.00000000 <- mask
  
  128.0 to 128.255
  10000000.00000000
  10000000.00000000

If you try to get another subnet, you'll get an error:
Error: Invalid function argument

  on main.tf line 50, in output "example":
  50:     value = cidrsubnets("10.1.0.0/16",1,1,1)

Invalid value for "newbits" parameter: not enough remaining address space for
a subnet with a prefix of 17 bits after 10.1.128.0/17.

Following the above logic, you can see that you can use 2 "newbits" to create 4 subnets.
output "example"{
    value = cidrsubnets("10.1.0.0/16",2,2,2,2)
}

example = [
  "10.1.0.0/18",
  "10.1.64.0/18",
  "10.1.128.0/18",
  "10.1.192.0/18",
]

==========================
  0.0 to 63.255  
  00000000.00000000
  11000000.00000000
  
  64.0 to 127.255
  01000000.00000000
  11000000.00000000
  
  128.0 to 191.255
  10000000.00000000
  11000000.00000000
  
  192.0 to 255.255
  11000000.00000000
  11000000.00000000

And so, using log base 2 to the desired number of subnets, you can calculate how many minimum "newbits" you need to use. 

Using cidrsubnet


However, a big issue here is calling cidrsubnets won't allow you to pass in dynamic number of arguments to create dynamic number of subnets. That's when you can use cidrsubnet and pass in the nth subnets.

This gives us the 4th subnet (it's zero based index):
output "example"{
    value = cidrsubnet("10.1.0.0/16",2,3)
}

example = 10.1.192.0/18

But of course tricky part of Terraform is that there isn't a convenient way to do an index for loop like this:
for (i=0; i<5; i++){

}


Index Looping


You can hack your way around this by creating a map or list and doing a for loop across it. 

Method 1: Pre-populated map of counter array. This method will use the desired index that matches the key in the counter_map. 
locals{
    counter_map={
        1=[0],
        2=[0,1],
        3=[0,1,2],
        4=[0,1,2,3],
        5=[0,1,2,3,4]
    }
    private_subnets = [for item in local.counter_map[var.private_count]: cidrsubnet(local.subnets[1], local.private_count_newbit, item)]
}

Method 2: One long counter array. This method pulls all the numbers up to the desired index value. 
locals{
    counter_set_all = [0,1,2,3,4,5,6,7,8,9]
    public_subnets = [for item in [for item in local.counter_set_all : item  if item < var.public_count]: cidrsubnet(local.subnets[0], local.public_count_newbit, item)]}

Putting all the pieces together


variable "public_count_weight"{
    default = 1
    description = "newbit weight given to subnet, lower means bigger subnet"
} 
variable "private_count_weight"{
    default = 1
    description = "newbit weight given to subnet, lower means bigger subnet"
}
    
variable "private_count"{
    default = 2
}

variable "public_count"{
    default = 2
}

locals{
    ## newbits can be calculated from the number of subnets desired by looking at the binary log to the desired subnet
    ## If 2 subnets are required, this requires 1 more bit in the netmask  (2^1 = 2 or log base 2 of 2 = 1)
    ## If 4 subnets are required, this requires 2 more bits in the netmask (2^2 = 4 or log base 2 of 4 = 2) 
    ## If 8 subnets are required, this requires 3 more bits in the netmask (2^3 = 8 or log base 2 of 8 = 3)
    public_count_newbit  = ceil(log(var.public_count  , 2 ))
    private_count_newbit = ceil(log(var.private_count , 2 ))
    ## split the initial CIDR into two, one for public and one for private
    subnets = cidrsubnets("10.1.0.0/16", var.public_count_weight, var.private_count_weight)
    ## Split each of the half from above for desired number of subnet in each type
    public_subnets = var.public_count == 1 ? [local.subnets[0]]:[for item in [for item in local.counter_set_all : item  if item < var.public_count]: cidrsubnet(local.subnets[0], local.public_count_newbit, item)]
    private_subnets = var.private_count == 1 ? [local.subnets[1]]:[for item in local.counter_map[var.private_count]: cidrsubnet(local.subnets[1], local.private_count_newbit, item)]
}

locals{
    counter_map={
        1=[0],
        2=[0,1],
        3=[0,1,2],
        4=[0,1,2,3],
        5=[0,1,2,3,4]
    }
    counter_set_all = [0,1,2,3,4,5,6,7,8,9]

}

output "subnets" {
    value = local.subnets
}

output "private"{
    value = local.private_subnets
}

output "public"{
    value = local.public_subnets
}


No comments:

Post a Comment

AWS WAF log4j query

How to query AWS WAF log for log4j attacks 1. Setup your Athena table using this instruction https://docs.aws.amazon.com/athena/latest/ug/wa...