Skip to content

Special file type for locals or overrides #37517

@djryanj

Description

@djryanj

Terraform Version

1.13.0

Use Cases

Consider the following:

  • Your Terraform configuration has multiple .tfvars files, each one dedicated to a specific environment
  • Your Terraform configuration requires locals, because some resource configurations need dynamic inputs based on other variables or references to data sources, etc.
  • Your Terraform configuration cannot be generalized enough such that global locals and environment-specific tfvars are sufficient without significant use of conditionals or other functions

For example, say I'm creating an Application Gateway in Azure, and I have listeners that are different in dev and prod and require me to reference different web application firewall policy ID's on a per-listener basis; my listeners are also not the same in dev and prod. Take the following (very simplified) configuration:

main.tf:

resource "azurerm_application_gateway" "network" {
  name                = "example-appgateway"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  firewall_policy_id  = azurerm_web_application_firewall_policy.default.id

  dynamic "http_listener" {
    for_each = local.http_listeners

    content {
      frontend_ip_configuration_name = coalesce(http_listener.value.frontend_ip_configuration_name, local.frontend_ip_configuration_name, local.frontend_ip_configuration_private_name)
      frontend_port_name   = http_listener.value.frontend_port_name
      name                 = http_listener.value.name
      protocol             = http_listener.value.ssl_certificate_name == null ? "Http" : "Https"
      firewall_policy_id   = http_listener.value.firewall_policy_id
    }
  }
}

dev.tfvars:

http_listeners = {
  listener_1 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener1"
  }
}

Desired final configuration:

http_listeners = {
  listener_1 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener1"
  }
}

prod.tfvars:

http_listeners = {
  listener_3 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener3"
  }
  listener_4 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener3"
  }
}

If I use locals, like this:

locals.tf:

locals {
  http_listeners = {
    for k, v in var.http_listeners : k => {
      name                           = v.name
      frontend_port_name             = v.frontend_port_name
      frontend_ip_configuration_name = v.frontend_ip_configuration_name
      firewall_policy_id             = v.firewall_policy_id
    }
  }
}

And my desired final configuration for local.http_listeners would then be:

dev:

http_listeners = {
  listener_1 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener1"
  }
}

prod:

http_listeners = {
  listener_3 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener3"
    firewall_policy_id             = azurerm_web_application_firewall_policy.listener_3.id
  }
  listener_4 = {
    frontend_ip_configuration_name = "feip-1"
    frontend_port_name             = "fep-80"
    name                           = "listener3"
    firewall_policy_id             = azurerm_web_application_firewall_policy.listener_4.id
  }
}

As you can see I have no easy way of defining the var.http_listeners input variable in a .tfvars file in such a way that I could correctly reference any of the azurerm_web_application_firewall_policy resources and have the final output be unique for dev and prod or whatever else I decide to create in the future.

Attempted Solutions

A few solutions I've considered:

  • Hard code ID's into the variables, which doesn't work if this module is also creating those resources plus it risks encoding information into the code that may be considered secret (like subscription IDs)
  • Really complicated conditionals and other things. This is hard to read, and doesn't scale well partly due to the lack of switch statements (hey, there's another idea for a feature) but even then, the scale is very limited
  • Use a module for this resource, and call it multiple times, but using a conditional where it only creates that configuration if e.g., environment = dev. This violates DRY, and forces configuration out of variables and into the code directly.
  • Use _override.tf files, which would be reasonable in an environment where pipelines are in use as they could be created/dropped in per environment, but causes difficulties when run locally
  • Requesting that .tfvars files be able to reference resources created in the configuration, which seems like the sort of thing that would lead to impossible race conditions, cycle errors, and other nightmares

Proposal

Currently, Terraform allows us to specify which tfvars file(s) to use by passing a -var-file CLI parameter (to some/most commands). The proposal here is to either:

  1. Extend that same behaviour into locals by defining a new file type, .tflocals, into which locals are placed, and a CLI parameter, -local-file, which works the same as -var-file.
    • Importantly, this should not break regular locals usage; just like with tfvars files, Terraform should ignore their existence unless they are specifically passed into the CLI at runtime except e.g., terraform.tfvars and similarly terraform.tflocals or perhaps locals.tflocals.
  2. Alternatively, do the same thing with a new .tfoverride file type, which works just like _override.tf files except again, is only processed by Terraform when explicitly passed in via an override-file CLI parameter. There should be no change to regular _override.tf file processing.

It's relevant to call out that I stated above when considering multiple modules called conditionally that

This violates DRY, and forces configuration out of variables and into the code directly

which arguably could apply here too if one considers locals to be part of the code rather than part of the variable definitions for the code. Personally I take the latter view, which is why I think creating a .tflocals file type for locals makes a certain amount of sense.

Thank you for considering this.

References

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions