Default values not being inherited from the default block - asks me to provide them as they are required

Context: I am trying to create a list of EC2 instances. I want to avoid defining every single value in my .tfvars file, and instead want them to be inherited from the default block, but it’s not happening.

Question: Could you advise me on how to make it happen?

Detailed Description:

I create EC2 instances by iterating through a variable that I created. This variable is a list of objects, where each object contains all the attributes necessary for EC2 creation.

Here is an example of the iteration:

module "aws_instances" {
  for_each = {
    for index, ec2 in var.ec2_instances : tonumber(index) => ec2
  }
  source                                       = "../modules/instance"
  account                                      = var.account
  repo                                         = var.repo
  environment                                  = var.environment
  zone                                         = var.zone
  name                                         = each.value.name
  prefix                                       = each.value.prefix
  disable_api_termination                      = each.value.disable_api_termination
  os_type                                      = each.value.os_type
  instance_type                                = each.value.instance_type
  instance_count                               = each.value.instance_count
  root_volume_size                             = each.value.root_volume_size
  root_volume_delete_on_termination            = each.value.root_volume_delete_on_termination
  additional_data_volume_size                  = each.value.additional_data_volume_size
  additional_data_volume_delete_on_termination = each.value.additional_data_volume_delete_on_termination
  network_volume_size                          = each.value.network_volume_size
  port_ranges                                  = each.value.port_ranges
  public_port_ranges                           = each.value.public_port_ranges
  additional_ip_address_ranges                 = each.value.additional_ip_address_ranges
  extra_tags                                   = each.value.extra_tags
  custom_ami                                   = each.value.custom_ami
  custom_amis                                  = each.value.custom_amis
  custom_ingress_cidr                          = each.value.custom_ingress_cidr
  optional_people_ip_address_ranges            = each.value.optional_people_ip_address_ranges
  optional_default_ports_ip_address_ranges     = each.value.optional_default_ports_ip_address_ranges
  enforce_throughput                           = each.value.enforce_throughput
  enforce_iops                                 = each.value.enforce_iops
}

Here is the variable itself, along with the default block:

variable "ec2_instances" {
  type = list(object({
    name                                         = string
    prefix                                       = string
    disable_api_termination                      = bool
    os_type                                      = string
    instance_type                                = string
    metadata_options                             = map(string)
    instance_count                               = number
    root_volume_size                             = number
    root_volume_delete_on_termination            = bool
    additional_data_volume_size                  = number
    additional_data_volume_delete_on_termination = bool
    network_volume_size                          = number
    port_ranges = list(object({
      description = string
      from_port   = number
      to_port     = number
      protocol    = optional(string)
    }))
    public_port_ranges = list(object({
      description = string
      from_port   = number
      to_port     = number
      protocol    = optional(string)
    }))
    additional_ip_address_ranges = list(string)
    extra_tags                   = map(string)
    custom_ami                   = bool
    custom_amis                  = map(string)
    custom_ingress_cidr = list(object({
      description = string
      from_port   = number
      to_port     = number
      protocol    = string
      cidr_blocks = string
    }))
    keep_subnet                              = bool
    optional_people_ip_address_ranges        = optional(list(string))
    optional_default_ports_ip_address_ranges = optional(list(string))
    enforce_subnet                           = optional(string)
    enforce_throughput                       = optional(number)
    enforce_iops                             = optional(number)
    hibernation                              = optional(bool)
  }))
  default = [
    {
      name                    = null
      prefix                  = "app"
      disable_api_termination = true
      os_type                 = "ubuntu-bionic"
      instance_type           = null
      metadata_options = {
        "http_endpoint" = "enabled"
        "http_tokens"   = "required"
      }
      instance_count                               = 1
      root_volume_size                             = 120
      root_volume_delete_on_termination            = false
      additional_data_volume_size                  = 0
      additional_data_volume_delete_on_termination = false
      network_volume_size                          = 0
      port_ranges                                  = []
      public_port_ranges                           = []
      additional_ip_address_ranges                 = []
      extra_tags                                   = {}
      custom_ami                                   = false
      custom_amis = {
        "windows"                                   = "ami-019f2455a29c94910"
        "amazon-linux2"                             = "ami-08ef2d900be37a0ee"
        "ubuntu-xenial-16.04-amd64-server-20190628" = "ami-03746875d916becc0"
      }
      custom_ingress_cidr = []
      keep_subnet         = false
    }
  ]}

The example of the .tfvars file where I am testing :

ec2_instances = [
  {
    name                    = "1234"
    custom_ami              = true
    instance_count          = 1
    instance_type           = "t3.small"
    disable_api_termination = false
    os_type                 = "amazon-linux2"

    root_volume_delete_on_termination = true
    extra_tags = {
      "serl:os-version" = "Amazon Linux 2"
    }
    custom_amis = {
      "windows"                                   = "1234"
      "amazon-linux2"                             = "1234"
      "ubuntu-xenial-16.04-amd64-server-20190628" = "1234"
    }
    optional_people_ip_address_ranges        = ["1234"]
    optional_default_ports_ip_address_ranges = ["1234"]
    custom_ingress_cidr = [
      {
        description = "SSH"
        from_port   = 22
        to_port     = 22
        protocol    = 6
        cidr_blocks = "1234"
      },
      {
        description = "SSH"
        from_port   = 22
        to_port     = 22
        protocol    = 6
        cidr_blocks = "1234"
      },
    ]
  }]

The error that I’m getting :

The resolved value of variable “ec2_instances” is not appropriate: element
0: attributes “additional_data_volume_delete_on_termination”,
“additional_data_volume_size”, “additional_ip_address_ranges”,
“keep_subnet”, “metadata_options”, “network_volume_size”, “port_ranges”,
“prefix”, “public_port_ranges”, and “root_volume_size” are required.

I don’t understand why are those required to be defined in the .tfvars file.

I thought the whole point of the default block was to save myself lines of code and not having to specify values in the .tfvars file if the same values are present in the default block.

I would really appreciate advice in how to achieve the goal whch is creation of any amount of ec2s through a tfvars file but not having to define the attributes in the default block in tfvars file if the value is the same.

Thank you for reading the thread

Hi @GJarzebakPrivate,

The default value for an input variable is used for situations where the caller does not assign a value to the input variable at all. You’ve assigned a value, so Terraform ignores the default.

From what you’ve described it seems like what you want instead is for certain individual attributes inside the nested object types to be optional and have default values, rather than the variable as a whole being optional and having a default value. If so, you can declare Optional Object Type Attributes (with optional default values to use when they aren’t set) as part of your type constraint.

Thank you for your answer.

I think I might have made a mistake.

So my goal is for the objects to have the default value but not for the list to let’s say have a default value of 1 object in it with attributes X,Y,Z

The values for in the .tfvars file will always have a list of objects in it (this might be a list of 3 - 5 - 10 objects , depends on environment ) and i wanted them (the objects in the list not the list) to have those default values if not provided

So let’s say I’d have an object with attributes

id = number
name = optional(string) (default value a)

Goal is if I give in .tfvars a list

ec2instances = [

{
id = 1
name = a
},
{
id = 2
} ]

then I’d like to end up with a list of 2 objects with ids 1 and 2 and names a

Hopefully it makes sense but am really unsure if i can achieve that kind of thing in terraform

When you write code in your forum posts, the forum is mangling it a bit. I wrote some advice about this in Welcome to the forum - please reformat your message

As to your actual question, @apparentlymart included a link to the relevant section of the Terraform documentation - please click through and read that section. In short, you would change your type declaration to include

  name = optional(string, "a")

You’re right about formatting but no I don’t think his answer is relevant to my problem (maybe in the first post I wasn’t specific enough)

No, it is relevant - but I do accept that the mention in the Terraform documentation about how to use optional() in type constraints is quite short and not so easy to read if you don’t already understand the topic.

This:

sounds like exactly what you appear to be trying to do, to me.

And this:

is the link to the relevant documentation.

What you need to do is modify the type = portion of your variable definition (and then delete the default = part entirely.

For example, instead of writing

  os_type = string

you should write

  os_type = optional(string, "ubuntu-bionic")

Unfortunately optional with two arguments is not supported in the TF I’m using. I think it treats my variable default value as a list of one object rather than list of X objects where each objects have default values

Then you should upgrade to a modern version of Terraform.

i know your kind

if you are not being helpful then why respond ?

do you think I can force my company to do an upgrade ?

Wow… you come here for free help and when the answer isn’t one you like, you spew insulting invective… by the way, previous versions of edited comments are still viewable, FYI…

Sorry to hear you work at a company which doesn’t listen to reasonable engineering advice from their engineers.

You make a statement:

which makes upgrading the absolute obvious thing to talk about next… but conspicuously omitted saying anything about it at all - so naturally I took the conversation in the only obvious direction.

@GJarzebakPrivate, please keep the discourse civil and on-topic here.

@maxb is correct in this case, the only reasonable solution would be to upgrade the version of Terraform which supports this feature.

Note there never was a supported Terraform release with optional without the default argument. That feature was only available via an experimental flag, but never released to production precisely because it could not handle situations like you have described.