TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

Variable Precedence, Outputs, and the Illusion of Sensitive Values

Continue in TT Lab

In one line

For the same code to handle dev, stage, and prod, the values have to move out of the code. The problem is that there are several places to put a value, and among them there is a fixed order of strength.

Why this was needed

Suppose there are three environments and the only differences are the instance size and the replica count. The easiest solution is to copy the directory three times. And half a year later, the three directories have become different creatures. A security setting that went only into prod is missing in stage, and a bug fixed in dev was not reflected in prod. Accidents always happen in those gaps.

Variables are the device that prevents this copying. If you keep the code as one and swap only the values, the difference between environments shrinks from "a diff of three files" to "a few values." Then a person can check the difference by eye.

How it works

There are several places to put a value, and the one that comes later wins.

Strength Place Nature
Weakest The default of the variable declaration "This value if nobody gives one"
Weak The environment variable TF_VAR_이름 (the suffix is the variable name) Injected from a CI runner or shell
Medium terraform.tfvars The standard values for this directory
A bit strong *.auto.tfvars (in filename dictionary order) Values read automatically along with it
Strong -var-file=파일 (the placeholder is a file) A bundle of values chosen for this run
Strongest -var 이름=값 (the placeholders are the name and the value) An exception for this one time

A variable declaration has three things to attach besides the value. type blocks a wrong shape early, description says what this value is, and validation pins the allowed range in code. The real worth of validation is moving the time of failure earlier. Without validation, a wrong value blows up mid-apply as a provider error, and by then half has already been created.

locals looks like a variable but is different in nature. It is a named expression that cannot be injected from outside. Use it for a value you want to define once and use in several places, such as a name prefix that combines several variables.

An output is this configuration's public interface. When used as a module, it is the only window that other code references, so it is good to attach a description to every output. And if you give the -json option, it comes out in a machine-readable format that is good to use in pipelines.

Finally, sensitive = true. It only hides the output on screen. The value remains in plain text in the state file, and it is also in output -json as it is. If you do not know this, you end up leaving the state file anywhere under the mistaken belief that "I marked it sensitive, so it's safe."

What you see in the field

First, the fire put out with -var and forgotten. If you saved the service during incident response with -var replicas=10, that value must be committed to the repository. If not, the next normal deployment silently reverts to the old value. The property that the command-line value is strongest is useful in an emergency, and it comes with the price that no record is left.

Second, the trap of defaults. If you put a default on every variable to "just make it run," then when no value is given, production is quietly created with development settings. For values that must differ per environment, it is safer not to give a default. When there is no value the tool asks or fails, and that failure is far cheaper than an accident.

Third, secrets left in the plan log. The plan output prints resource attributes as they are. If you pile CI logs up somewhere anyone in the company can see, that is a leak path. That is why, separately from the sensitive marking, you must also look at the log retention policy.

Being sure where a value comes from

There are six channels through which you can give a value to one variable. If you do not know the precedence, you will experience "I definitely changed it, but it didn't change." The further down, the stronger.

  1. The default (default)
  2. The environment variable TF_VAR_이름 (the suffix is the variable name)
  3. terraform.tfvars
  4. *.auto.tfvars (in file name order)
  5. A file given with -var-file=
  6. A value given directly with -var=

Writing twice in the same file is an error, but with different channels it is silently overridden. If CI has planted TF_VAR_ but you are editing terraform.tfvars locally, the third beats the second, so it works locally and not in CI. To check where a value came from, get the plan as a file and read it.

terraform plan -out=tfplan
terraform show -json tfplan | jq '.variables'

Attach validation to the variable declaration. If you meet a wrong value in the middle of an apply, half has already been created.

variable "env" {
  type = string
  validation {
    condition     = contains(["dev", "stg", "prod"], var.env)
    error_message = "env 는 dev, stg, prod 중 하나여야 합니다."
  }
}

sensitive = true hides only the screen. The values go into the state file and the plan file as they are. Instead of passing secrets as variables, it is better to read them from a secret store with a data source, and even then, treat the repository on the assumption that they remain in the state.

An output is the interface you leave for the next person. The users of a module come to depend on its outputs, so once decided, they are hard to change. Instead of exporting the internal resource name as it is, name by meaning: compared with instance_id, app_server_id lasts longer, and compared with ip, private_endpoint does.

terraform output -json | jq -r '.app_endpoint.value'

What you will do in the next lab

In /root/tf/vars, you put a value into the same variable in four ways, the default, tfvars, the command line, and an environment variable, and confirm in files which one wins. You reject a wrong value with validation and save that message. You combine a name prefix with locals, and create four or more outputs with descriptions. Finally you declare a sensitive variable and see with your own eyes that it is hidden in the output a person reads but the value remains in the JSON.