Terraform Associate 004 #7 Writing Configuration: Variable Precedence, Functions, and Custom Conditions

5 min read

Most of the configuration-writing domain is syntax you already know from Basics #3 and #4, but two things are different. The exam asks for the exact order of precedence, and 004 asks head-on about the custom conditions the track only brushed past. We will compress the familiar parts quickly and spend the space on the reinforcement.

Variables: types and precedence #

The types are string, number, and bool, plus the collections (list, set, map) and the structural types (object, tuple), and a type violation is rejected before plan. The exam staple is the precedence of value sources. Whatever is evaluated later wins.

Variable precedence
(weakest) environment variables TF_VAR_name
        → terraform.tfvars
        → terraform.tfvars.json
        → *.auto.tfvars (alphabetical by file name)
        → -var, -var-file (command line, in the order given)  (strongest)

Two items here never appeared in the track. *.auto.tfvars files are loaded automatically regardless of their name, and tfvars also has a JSON form. For the “what if the same variable is defined in several places?” question, the answer is that whatever sits lower on this ladder (closer to the command line) wins. This is also the moment to recall that sensitive = true only masks plan output while state still holds plaintext (#5).

output and locals: a compressed review #

output is the exit for values: it can be re-read with the terraform output command, it accepts a sensitive marker of its own, and it is the passage between modules (#6). locals attach names to internal computations, and the small trap is the singular-versus-plural difference: declaration uses a locals block, references use local.name. The rule from Basics #3 — values users will change go in variable, internal combinations go in locals — is the answer to the design questions.

Functions and expressions: only the exam staples #

The first exam point is the premise that you cannot define your own functions — only built-ins exist. Narrowed down to what actually shows up in questions:

  • Collections: length, merge (map merge, later wins), lookup (key lookup in a map with a default), toset (conversion for for_each)
  • Strings and encoding: format, join/split, jsonencode (an IAM policy staple), base64encode
  • Files: file (read contents), templatefile (read while substituting variables, a user_data staple)
  • Type checks and conditionals: can (whether an expression succeeds), try (fallback value on failure)

On the expression side, the conditional a ? b : c, for expressions (brackets = list, braces = map), and the splat [*] are the whole list, exactly as in Basics #4. That terraform console is the tool for experimenting with expressions also comes up as a command question.

Reinforcement: the three kinds of custom conditions #

This is an area 004 strengthened. Depending on where the condition is attached there are three kinds, and you distinguish them by what they validate.

  1. Variable validation: input validation. This is the one you practiced in Basics #3, evaluated at the moment the value comes in.

  2. precondition and postcondition: validation of assumptions, attached inside the lifecycle block of a resource, data source, or output.

main.tf
data "aws_ami" "al2023" {
  # ... lookup filters ...

  lifecycle {
    postcondition {
      condition     = self.architecture == "x86_64"
      error_message = "The AMI returned by the lookup must use the x86_64 architecture."
    }
  }
}

resource "aws_instance" "web" {
  ami = data.aws_ami.al2023.id

  lifecycle {
    precondition {
      condition     = data.aws_ami.al2023.state == "available"
      error_message = "The AMI must be in the available state before the instance can be created."
    }
  }
}

A precondition is an assumption that must hold before the object is evaluated (usually a check on something it depends on); a postcondition is a guarantee the evaluated result must satisfy, and inside a postcondition you reference the object’s own attributes via self. On failure, plan and apply stop with an error, so this is the mechanism by which the code itself prevents infrastructure from being built on a wrong premise.

  1. check blocks: continuous validation of the infrastructure. A top-level check block holds asserts, and the decisive difference is that a failure does not block apply — it only leaves a warning. The use case is “do not block the deployment, but keep watching the state” (checking whether a certificate is close to expiry, for example).

Comparison questions run on two axes: what is validated (input value / assumption and result / ongoing state) and the effect of failure (block / block / warn). With those two axes in place, any combination question about the three mechanisms is solvable.

Recap #

What we covered in this post:

  • Variable precedence runs environment variables → tfvars → auto.tfvars → command line, and later wins. The auto-loading of auto.tfvars is the unfamiliar item
  • Only built-in functions exist, and merge, lookup, templatefile, jsonencode, try, and can are the exam staples
  • validation checks input values and blocks on failure
  • precondition is a pre-evaluation assumption, postcondition is a post-evaluation guarantee that can reference self, and both block on failure
  • check blocks are continuous validation and are the only ones that leave a warning without blocking apply

In the next post (#8 Deep Dive on the New Scope), we bundle 004’s flagship new scope — ephemeral values and write-only arguments — together with the depends_on and lifecycle meta-arguments.

X