Best way to define subnets

When defining a VPC, I like to make the VPC’s cidr configurable, but then the private and public subnets should be configurable too. Ideally there would be a function or module where I can specify “I want this CIDR to be subdivided into 1 public and 1 private subnets, and each one of those to be subdivided into 3 subnets”, and should get 2 lists of 3 subnets each.

I tried the cidrsubnet and cidrsubnets but I find these rather complicated to use. I also tried which is a bit better, but I don’t think in terms of number of bits, rather in terms of number of subnets, so it’s still too much work :wink: And actually, working with number of bits assumes you know whether the CIDR will be /8, /16 or /24 etc.

Any suggestions?

something like this

variable "parent_cidr" {
  description = "Classless Inter-Domain Routing (CIDR) block"
  default     = ""

variable "number_of_subnets" {
  description = "Number of subnets to split the CIDR into. Hint: use even numbers, 2 or 4 work best"
  default     = 4

resource "null_resource" "cidr_subnets" {
  count = var.number_of_subnets

  triggers = {
    subnet = cidrsubnet(


output "subnets" {
  value = null_resource.cidr_subnets.*.triggers.subnet

should return results of

subnets = [

Four or two are good numbers to use for “number_of_subnets”. Other even numbers should return valid subnets but might not use the entire “parent_cidr” space.


A while back I wrote a module terraform-cidr-addr-plan which has a more opinionated take on systematic address planning, which seems like it might at least be a reasonable starting point for what you are describing here.

I’ve not had the time to “finish up” this module and publish it in the registry, and so I wouldn’t suggest depending on it directly in that repository, but you could perhaps use it as a starting point for a module of your own which meets your needs more directly.

One specific think I’ll note about terraform-cidr-addr-plan is that it uses the log function to calculate the number of bits required for a particular number of networks using a base-2 logarithm, and so if that was the only thing you didn’t like about hashicorp/subnets/cidr then you could use a similar technique to populate its new_bits attributes:

module "subnet_addrs" {
  source = "hashicorp/subnets/cidr"

  base_cidr_block = ""
  networks = [
      name     = "foo"
      new_bits = log(4, 2) # enough bits for four subnets
      name     = "bar"
      new_bits = log(2, 2) # enough bits for two subnets

With that said, hashicorp/subnets/cidr is really aimed at explicit address assignment rather than systematic address assignment, and that’s why it directly exposes the details about the address space. My other module linked above takes a higher-level approach where it assumes you want to allocate systematically across regions and availability zones, and so it requires a lot less configuration to get a network plan out but in return it’s less flexible about making variations in sizes of assignments for different use-cases.

Thanks @gpanula, that would be quite a bit of code to write every time I’m creating a new app stack config (ie not replicating an existing one), since I need this twice (one for public, one for private). But there are a couple of tricks there I did not know, so I’ll bear it in mind.

Thanks @apparentlymart I’ll have a look at that module. The log base 2 approach is interesting.