NovuSpark
All articles

October 31, 2025 · NovuSpark Team

Mastering Terraform Variables and Outputs

This is the second post in our Terraform fundamentals series. If you haven't already, start with Terraform 101.

The configuration from our last post worked, but it only worked for one specific instance, in one specific region, with one specific AMI hardcoded directly into the resource block. Copy that file to stand up a second environment, and you're now maintaining two nearly-identical files that will inevitably drift apart the first time someone updates one and forgets the other.

Variables and outputs are the fix — and they're also where most of the actual design decisions in a Terraform codebase live.

Input variables: parameterizing configuration

A variable is declared in its own block, and referenced anywhere in your configuration with var.<name>.

variable "instance_type" {
  type        = string
  description = "EC2 instance type for the web server"
  default     = "t3.micro"
}
 
variable "environment" {
  type        = string
  description = "Deployment environment name"
 
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "environment must be one of: dev, staging, production."
  }
}
 
resource "aws_instance" "web" {
  ami           = "ami-0c1a7f89451184c8b"
  instance_type = var.instance_type
 
  tags = {
    Name        = "web-${var.environment}"
    Environment = var.environment
  }
}

A few details here matter more than they look:

  • type is not optional in practice, even though Terraform will accept a variable without one. An untyped variable accepts anything, which means a typo ("tru" instead of true) fails at apply time deep inside a provider, with a confusing error, instead of failing immediately and clearly at plan time.
  • validation blocks turn a bad input into a clear error message instead of a confusing downstream failure. Without the block above, passing environment = "prod" (instead of "production") wouldn't fail until something else broke because of the mismatch — possibly much later, and possibly in someone else's PR.
  • default is a design decision, not just a convenience. A variable with no default is required at every call site — good for values that genuinely differ per environment (like environment itself). A sensible default is appropriate for values most callers shouldn't have to think about.

Setting variable values

Terraform looks for values in a defined precedence order, which is worth knowing so you're never surprised by which one "wins":

  1. Command-line -var or -var-file flags (highest precedence)
  2. *.auto.tfvars files, loaded automatically
  3. TF_VAR_<name> environment variables
  4. The default in the variable block itself (lowest precedence)

In practice, most teams settle on one primary mechanism per context — a terraform.tfvars file for local development, and CI-injected TF_VAR_ environment variables for anything that shouldn't be committed to version control (this matters a lot once secrets enter the picture — more on that in the state management post).

# terraform.tfvars
environment   = "staging"
instance_type = "t3.small"
terraform apply -var-file="terraform.tfvars"

Outputs: exposing values for the next step

An output surfaces a value after apply completes — either for a human to read, or for another Terraform configuration (or CI pipeline step) to consume.

output "instance_public_ip" {
  value       = aws_instance.web.public_ip
  description = "Public IP address of the web server"
}
 
output "instance_id" {
  value       = aws_instance.web.id
  description = "EC2 instance ID"
}

Run terraform apply, and these values print at the end:

Outputs:

instance_id       = "i-0abcd1234efgh5678"
instance_public_ip = "18.130.42.101"

They're also queryable independently, without a full apply:

terraform output instance_public_ip

This is the detail that makes outputs genuinely useful rather than just a nicer print statement: a deployment script in CI can run terraform output -json and pipe real infrastructure values — a load balancer DNS name, a database endpoint, a queue URL — directly into the next step of a pipeline, without anyone copy-pasting a value out of a console.

Marking sensitive outputs

output "db_password" {
  value     = random_password.db.result
  sensitive = true
}

sensitive = true stops Terraform printing the value in plan and apply output. It is not encryption, and it does not remove the value from the state file — it only prevents it showing up in a terminal log or CI output where someone might screenshot it or it might get archived somewhere it shouldn't. Real secrets still need real secrets management; we'll come back to exactly why state itself needs protecting in the remote state post.

A pattern worth adopting early: locals

Once a configuration has more than a couple of variables, you'll often want a computed value derived from them, without turning it into another input the caller has to think about. That's what locals are for:

locals {
  name_prefix = "novuspark-${var.environment}"
}
 
resource "aws_instance" "web" {
  ami           = "ami-0c1a7f89451184c8b"
  instance_type = var.instance_type
 
  tags = {
    Name = "${local.name_prefix}-web"
  }
}

locals aren't set by the caller — they're internal helper values computed once, referenced with local.<name> throughout the rest of the file. Reaching for a local instead of repeating a string-interpolation expression five times is one of the simplest ways to keep a growing configuration readable.

What to actually remember from this post

  • Type and validate every variable that isn't purely internal — it turns a confusing failure three steps later into a clear one immediately.
  • Understand the precedence order for variable values before you're debugging why a -var flag isn't taking effect.
  • Outputs are an integration point, not just a print statement — they're how Terraform hands real values to whatever comes next.
  • sensitive = true hides a value from logs, it doesn't secure it — the value is still sitting in state in plaintext.

Next in the series: Terraform Modules: Building Reusable Infrastructure, where this single-file configuration becomes a reusable building block.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.