1.2.2.1. Terraform Input Variable Files

variables.tf is also one of the four files a Terraform module must always contain (main.tf / variables.tf / output.tf / terraform.tf). It contains all input variable blocks of the module. Even if the module has no variable blocks at all, you should still keep an empty variables.tf file. Multiple input variable files are allowed, but each filename must contain the substring variables, for example: variables_db.tf or db_variables.tf. Also, these variables files should contain only variable blocks.

If we think of a module as a function, then variables.tf is its parameter list. A good module should follow certain conventions for its input variables:

1.2.2.1.1. All input variables must be declared in variables.tf

1.2.2.1.2. All input variables must have an accurate type

1.2.2.1.3. All input variables must have an accurate description

1.2.2.1.4. Input variables that receive Ephemeral data must declare ephemeral = true

1.2.2.1.5. Input variables that accept sensitive data (e.g. passwords) must declare sensitive = true

1.2.2.1.6. Sensitive data should be provided via a dedicated input variable rather than grouped inside an object

Consider this input variable:

variable "virtual_machine" {
  type = map(object({
    name     = string
///...
    admin_password = string
  }))
  description = "Virtual Machine"
  sensitive   = true
}

In this variable, admin_password is sensitive data, but it is defined together with other members inside an object type. Terraform currently does not support marking a single attribute of an object as sensitive; we have no choice but to mark the entire variable as sensitive. The downside is that during terraform plan and terraform apply, Terraform will hide the entire object's values, making it impossible to see other (non-sensitive) attributes. More troublesome is the following situation:

resource "azurerm_windows_virtual_machine" "example" {
  for_each = var.virtual_machine

  name                = each.value.name
# ...
  admin_password      = each.value.password
}

When we assign a value to var.virtual_machine through a terraform.tfvars file and then run terraform plan, we'll see:

╷
│ Error: Invalid for_each argument
│
│   on main.tf, in resource "azurerm_windows_virtual_machine" "example":
│   66:   for_each = var.virtual_machine
│     ├────────────────
│     │ var.virtual_machine has a sensitive value
│
│ Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value
│ could be exposed as a resource instance key.
╵

Now we cannot use for_each to create resources. We must separate admin_password into its own variable.

1.2.2.1.7. Ephemeral or Sensitive variables must not define a default value

Setting default values for sensitive data (such as passwords or API keys) may allow these values to be guessed. Attackers might try defaults found in open-source code. If users aren't aware of this, our module could become an entry point for attacks.

1.2.2.1.8. For collection-typed variables, prefer using an empty collection as default rather than null

Using an empty collection avoids extra conditional logic inside the module. For example, when using for_each to create resources, an empty collection results in creating no resources, while null may cause errors or require additional condition checks.

1.2.2.1.9. If possible, declare nullable = false to simplify downstream logic

In Terraform, input variables can be explicitly set to null. This can cause problems with certain types like bool, e.g. var.log_enabled:

variable "log_enabled" {
  type        = bool
  description = "Whether to turn log on or not"
}

During runtime the value of var.log_enabled could be true / false / null. If we write:

count = var.log_enabled ? 1 : 0

Then when the value is null it triggers a runtime error, so we must write:

count = var.log_enabled == true ? 1 : 0

Even adding a default does not fix this:

variable "log_enabled" {
  type        = bool
  default     = true
  description = "Whether to turn log on or not"
}

If the caller explicitly sets the variable to null, the runtime value is still null.

If we set nullable = false, then when the caller sets it to null the default is used instead. The state space is reduced to just true / false, reducing errors and simplifying logic.

1.2.2.1.10. For variables used with for_each to create multiple resources, prefer map over set, and clearly state in the description that map keys must be static literals, not dynamic values

The reason not to use set is that set(object) uses the entire object's data values to determine uniqueness. Terraform does not allow using such a set of objects as a for_each source.

map in Terraform only supports string keys, so it's an excellent data type to pair with for_each to create multiple resource blocks. But we must ensure the keys are static. What does that mean? Consider this code:

variable "groups" {
  type = map(object({
    location = string
    tags = map(string)
  }))
}

resource "random_string" "random" {
  for_each = var.groups
  length   = 6
  special  = false
  upper    = false
}

locals {
  resource_groups = {
    for k, v in var.groups : "${k}-${random_string.random[k].result}" => {
      name     = "rg-${random_string.random[k].result}"
      location = v.location
      tags     = v.tags
    }
  }
}

resource "azurerm_resource_group" "main" {
  for_each = local.resource_groups
  name     = each.value.name
  location = each.value.location
  tags     = each.value.tags
}

This code throws an error at runtime:

╷
│ Error: Invalid for_each argument
│
│   on main.tf line 51, in resource "azurerm_resource_group" "main":
│   51:   for_each = local.resource_groups
│     ├────────────────
│     │ local.resource_groups will be known only after apply
│
│ The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot 
│ determine the full set of keys that will identify the instances of this resource.
│
│ When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place      
│ apply-time results only in the map values.
│
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on,   
│ and then apply a second time to fully converge.
╵

Terraform requires the keys of for_each to be known during the Plan phase, but in our code the keys include the output of random_string, which is only known after a successful Apply.

This sample is intentionally simplified for comprehension, but in real module usage users may not realize the keys passed into the map contain values not yet known. Therefore we recommend explicitly documenting in the description: ensure keys are static.

1.2.2.1.11. Do not declare values identical to Terraform defaults, e.g. sensitive = false, nullable = true

Redundant code that restates defaults provides no value; keep code minimal.

1.2.2.1.12. Order of variable definitions

Variables should follow this order:

  1. All required ones, sorted alphabetically
  2. All optional ones, sorted alphabetically

  3. A variable that defines a default is optional; otherwise it's required.

1.2.2.1.13. Naming variables

Ensure the name, description, and any validation of a variable align (as far as clarity allows) with upstream/downstream resource and data definitions. Ensure variables with the same purpose across different modules use the same naming.

You may add a prefix ending with _ to distinguish purposes. For example:

resource "azurerm_linux_virtual_machine" "webserver" {
  name                = "webserver"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  source_image_id     = var.webserver_source_image_id
  ...
}

Here in webserver_source_image_id, the source_image_id suffix matches the resource argument name (we assign webserver_source_image_id to source_image_id, both sides share the suffix). The webserver_ prefix is valid.

If a variable is used to pass a value to a resource argument, the variable's name should directly reuse that argument name (prefix allowed). The description should copy the relevant description from the resource's documentation whenever possible.

For boolean feature toggles, use affirmative names: xxx_enabled instead of xxx_disabled to avoid double negatives like !xxx_disabled.

Prefer xxx_enabled over enable_xxx.

1.2.2.1.14. validation

Input variable validation allows defining checks for input values. We allow and encourage module authors to use validation to block invalid inputs and provide clearer error messages. However, we discourage re-implementing validations already enforced by the Provider's schema (e.g., a field matching a regex). Duplicating such logic offers no extra value.

If you define validations, error_message must clearly state what input is (not) expected.

results matching ""

    No results matching ""