terraform-aws-modules/terraform-aws-vpc

Specifying use_ipam_pool = true sets provided cidr to null.

Closed this issue · 7 comments

Description

Try module config as specified in

Terraform module sets cidr to null if use_ipam_pool is true:

This results following error when trying to execute module configured with ipam and given CIDR:

aws_vpc_ipam_preview_next_cidr.this: Creating...
╷
│ Error: allocating cidr from IPAM pool (ipam-pool-01c77e7ea1ea0783d): operation error EC2: AllocateIpamPoolCidr, https response error StatusCode: 400, RequestID: 606540c0-39fe-4e67-8e12-afd98549f252, api error InvalidParameterCombination: The request cannot contain empty CIDR and Netmask Length for a pool with no default allocation netmask length set. Exactly one of them should be provided.
│ 
│   with aws_vpc_ipam_preview_next_cidr.this,
│   on main.tf line 80, in resource "aws_vpc_ipam_preview_next_cidr" "this":
│   80: resource "aws_vpc_ipam_preview_next_cidr" "this" {
│ 
╵
╷
│ Error: creating EC2 VPC: operation error EC2: CreateVpc, https response error StatusCode: 400, RequestID: 42c6d28f-5e47-4f92-9897-c9052ec7000b, api error InvalidParameterCombination: The request cannot contain empty CIDR and Netmask Length for a pool with no default allocation netmask length set. Exactly one of them should be provided.
│ 
│   with module.vpc_ipam_set_cidr.aws_vpc.this[0],
│   on .terraform/modules/vpc_ipam_set_cidr/main.tf line 28, in resource "aws_vpc" "this":
│   28: resource "aws_vpc" "this" {

Versions

  • Module version [Required]: 5.15.0
  • Terraform version: 1.7.5
  • Provider version(s): 5.75.0

Reproduction Code [Required]

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.46"
    }
  }
}

provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {}

locals {
  name   = "ex-${basename(path.cwd)}"
  region = "eu-west-1"

  azs               = slice(data.aws_availability_zones.available.names, 0, 3)
  preview_partition = cidrsubnets(aws_vpc_ipam_preview_next_cidr.this.cidr, 2, 2, 2)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-vpc"
    GithubOrg  = "terraform-aws-modules"
  }
}

################################################################################
# VPC Module
################################################################################

# IPv4

module "vpc_ipam_set_cidr" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${local.name}-set-cidr"

  use_ipam_pool     = true
  ipv4_ipam_pool_id = aws_vpc_ipam_pool.this.id
  cidr              = "10.1.0.0/16"
  azs               = local.azs

  private_subnets = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
  public_subnets  = ["10.1.11.0/24", "10.1.12.0/24", "10.1.13.0/24"]

  tags = local.tags
}


resource "aws_vpc_ipam" "this" {
  operating_regions {
    region_name = local.region
  }

  tags = local.tags
}

# IPv4
resource "aws_vpc_ipam_pool" "this" {
  description                       = "IPv4 pool"
  address_family                    = "ipv4"
  ipam_scope_id                     = aws_vpc_ipam.this.private_default_scope_id
  locale                            = local.region
  

  tags = local.tags
}

resource "aws_vpc_ipam_pool_cidr" "this" {
  ipam_pool_id = aws_vpc_ipam_pool.this.id
  cidr         = "10.0.0.0/8"
}

resource "aws_vpc_ipam_preview_next_cidr" "this" {
  ipam_pool_id = aws_vpc_ipam_pool.this.id
  netmask_length = 16
  depends_on = [
    aws_vpc_ipam_pool_cidr.this
  ]
}

Steps to reproduce the behavior:

Save terraform code and run
terraform plan -out a.out
terraform apply a.out

Expected behavior

VPC created without a problem if default netmask is not set on IPAM pool. User of this module may instead set netmask length on preview resource.

Actual behavior

Error message received as described above. This really prevents doing CIDR math beforehand because module sets given CIDR to zero. There is no technical limitation or reason on AWS provider side to do so.

Terminal Output Screenshot(s)

Additional context

If one was to remove setting cidr to null, VPC is created successfully!

  # cidr_block          = var.use_ipam_pool ? null : var.cidr
  cidr_block          = var.cidr

That's not how IPAM works, IPAM should create the VPC CIDR, not you

Actually it does. Using preview resource. I use this preview resource and take its CIDR and then calculate my subnets, pretty much like in example 1 in your module. But instead of letting VPC now ask for new CIDR, I provide one I got from preview resource.

And this error btw happens in your example too if IPAM pool does not have default subnet mask set. According to response received from AWS API, both approaches seem to be correct, either specifing CIDR that is in valid pool range or by specifying netmask. IPAM checks is CIDR asked is available and if it does, it creates allocation and links it to VPC. Forcing CIDR to null is crippling indented use I think

Btw, awesome speed in answering @bryantbiggs !
Kindly, please let me know if this can be addressed. If not, I might need temporarily fork this repo for my use case.

Siim

you have a couple issues here:

  1. Your IPAM configurations are not setup correctly, that is the errors that you are seeing. IPAM is unable to preview the next available CIDR and therefore is unable to pass a CIDR to this module
  2. You cannot set use_ipam_pool and cidr_block - the former tells the module/resources that IPAM will be used to fetch the appropriate CIDR, the latter is just simply a static CIDR that the module has no context as to where it came from.

You first need to sort out your IPAM errors, but even then - don't pass a CIDR to the module when IPAM should be used to generate one. If you fix the first error, you most likely will then run into an error where IPAM generates a CIDR that doesn't contain the subnet CIDRS - this is why our example is all dynamic because the base CIDR is not known until IPAM provides one and then everything is derived off of that CIDR

Thanks for quick reply. I'll try to elaborate:
I provided this TF code as example for quick demonstration of error in question, this is not how I use IPAM here. Dynamic example btw here requires first targeted plan and apply because it fails to get required counts for subnets in module. I wanted to prevent this in code demonstrating actual issue..

In reality, I do get CIDR from IPAM using resource "aws_vpc_ipam_preview_next_cidr" , abbreviavated code is approx like this:

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.52" 
    }    
    
  }
}


provider "aws" {
  region = var.region
}

variable region {
  type        = string
  default     = "eu-central-1"
  description = "AWS region"
}

variable name {
  type        = string
  default     = "default-vpc"
  description = "VPC name"
}

# Subnets info
variable private_subnets_count {
  type        = number
  default     = 3
  description = "Number of private subnets. Cannot have more private subnets than availability zones. If zero, then NatGW is not created"
}

variable private_subnets_length {
  type        = number
  default     = 24
  description = "Netmask length of private subnets"
}

variable public_subnets_count {
  type        = number
  default     = 3
  description = "Number of public subnets"
}

variable public_subnets_length {
  type        = number
  default     = 24
  description = "Netmask length of public subnets"
}

data "aws_region" "current" {}
data "aws_availability_zones" "available" {
  state = "available"
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}


locals {  

  
  region = data.aws_region.current.name
  
  private_subnets_count = length(data.aws_availability_zones.available.names) < var.private_subnets_count ? length(data.aws_availability_zones.available.names) : var.private_subnets_count

  vpc_cidr_length = 32 - ceil(log(local.private_subnets_count*pow(2,32-var.private_subnets_length) + var.public_subnets_count*pow(2,32-var.public_subnets_length), 2))

  _sn = var.private_subnets_length < var.public_subnets_length ? [
    {length = var.private_subnets_length, count = local.private_subnets_count},
    {length = var.public_subnets_length, count = var.public_subnets_count}
  ] : [    
    {length = var.public_subnets_length, count = var.public_subnets_count},
    {length = var.private_subnets_length, count = local.private_subnets_count}
  ]

  
  _sn_a = [
     for ix, config in range(local._sn[0].count): 
      cidrsubnet(local.vpc_cidr,local._sn[0].length - local.vpc_cidr_length, ix)
  ]

  _sn_b = [
     for ix, config in range(local._sn[1].count):       
       cidrsubnet(
          local.vpc_cidr,
          local._sn[1].length - local.vpc_cidr_length, 
          (local._sn[0].count * pow(2,(local._sn[1].length - local._sn[0].length)))+ix 
        )      
  ]
  
  ipam_pool_description = "o-${local.region}"
  vpc_cidr = aws_vpc_ipam_preview_next_cidr.current.cidr

  # all_subnets = cidrsubnets(local.vpc_cidr, 2,2,2,2)
  private_subnets = var.private_subnets_length < var.public_subnets_length ? local._sn_a : local._sn_b
  public_subnets = var.private_subnets_length < var.public_subnets_length ? local._sn_b : local._sn_a

  
}


data "aws_vpc_ipam_pool" "pool" {
  
  filter {
    name   = "description"
    values = [local.ipam_pool_description]
  }

  filter {
    name   = "address-family"
    values = ["ipv4"]
  }
}


resource "aws_vpc_ipam_preview_next_cidr" "current" {
  
  ipam_pool_id   = data.aws_vpc_ipam_pool.pool.id
  netmask_length = local.vpc_cidr_length

}


resource "aws_vpc" "this" {
  cidr_block          = local.vpc_cidr
  ipv4_ipam_pool_id   = data.aws_vpc_ipam_pool.pool.id
  #ipv4_netmask_length = var.ipv4_netmask_length


  enable_dns_hostnames                 = true
  enable_dns_support                   = true
  
  tags = { "Name" = var.name }
}

We have for each region created regional IPAM pool with description o-<region>, for example o-eu-central-1 and these pools are shared to organization with RAM.

I do not make use of calculated local variables private_subnets and public_subnets in this snippet but they are here to demonstrate why they are here:
They are here for simplifying use of module - operator does not need to know in advance size of cidr it tries to allocate, this is calculated based on number of subnets needed and their size.

This code gets CIDR from IPAM and it gets allocated for VPC just fine, so one can in fact specify CIDR to aws_vpc resource while at same time specifying IPAM pool to use.

My original code in issue description was to demonstrate just the issue I think module has.

Created vpc resource will look like this:

# aws_vpc.this:
resource "aws_vpc" "this" {
    arn                                  = "arn:aws:ec2:eu-central-1:<redacted>:vpc/vpc-005bf19926d6379b5"
    assign_generated_ipv6_cidr_block     = false
    cidr_block                           = "10.72.0.0/24"
    default_network_acl_id               = "acl-00939733d5dc9f207"
    default_route_table_id               = "rtb-04f046a49572e9a6b"
    default_security_group_id            = "sg-08286a47fe9160232"
    dhcp_options_id                      = "dopt-0fad892e7c477b992"
    enable_dns_hostnames                 = true
    enable_dns_support                   = true
    enable_network_address_usage_metrics = false
    id                                   = "vpc-005bf19926d6379b5"
    instance_tenancy                     = "default"
    ipv4_ipam_pool_id                    = "ipam-pool-05302d90109dbcf48"
    ipv6_netmask_length                  = 0
    main_route_table_id                  = "rtb-04f046a49572e9a6b"
    owner_id                             = "<redacted>"
    tags                                 = {
        "Name" = "dev-vpc"
    }
    tags_all                             = {
        "Name" = "dev-vpc"
    }
}

Thanks

This issue has been automatically marked as stale because it has been open 30 days
with no activity. Remove stale label or comment or this issue will be closed in 10 days

This issue was automatically closed because of stale in 10 days