# Variables, Outputs and Locals

## 1. Input Variables

Variables make a configuration reusable across environments and modules.

```hcl
variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging or prod."
  }
}

variable "instance_count" {
  type    = number
  default = 2
  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 20
    error_message = "instance_count must be between 1 and 20."
  }
}

variable "db_password" {
  type      = string
  sensitive = true          # redacted in plan/apply output (still stored in state!)
  nullable  = false
}

variable "services" {
  type = map(object({
    image    = string
    cpu      = optional(number, 256)       # optional attributes with defaults
    memory   = optional(number, 512)
    public   = optional(bool, false)
  }))
}
```

Types: `string`, `number`, `bool`, `list(T)`, `set(T)`, `map(T)`, `object({...})`, `tuple([...])`, `any`. Strong types + `validation` blocks catch mistakes at plan time instead of during a half-finished apply.

## 2. Setting Variable Values (Precedence, Low → High)

1. `default` in the variable block
2. environment variables `TF_VAR_name`
3. `terraform.tfvars`, then `*.auto.tfvars` (alphabetical)
4. `-var-file=prod.tfvars`
5. `-var 'name=value'` on the command line

```hcl
# prod.tfvars
environment    = "prod"
instance_count = 6
services = {
  api    = { image = "ghcr.io/acme/api:1.4.2", cpu = 512, memory = 1024, public = true }
  worker = { image = "ghcr.io/acme/worker:1.4.2" }
}
```

Never put secrets in `.tfvars` files committed to Git; pass them via `TF_VAR_` from a secret store in CI, or better, have Terraform reference the secret manager directly (data source or managed passwords).

## 3. Locals

`locals` are named expressions — computed once, used many times. They keep configurations DRY and readable:

```hcl
locals {
  name_prefix = "${var.project}-${var.environment}"
  is_prod     = var.environment == "prod"
  common_tags = {
    project     = var.project
    environment = var.environment
    owner       = var.team
  }
  public_services = { for k, s in var.services : k => s if s.public }
  azs             = slice(data.aws_availability_zones.available.names, 0, local.is_prod ? 3 : 2)
}
```

Variables are **inputs from outside**; locals are **internal derivations**. If a value shouldn't be overridden by callers, make it a local.

## 4. Outputs

Outputs expose values to the CLI, to calling modules, and to other configurations:

```hcl
output "alb_dns_name" {
  description = "Public DNS name of the load balancer"
  value       = aws_lb.main.dns_name
}

output "db_endpoint" {
  value     = aws_db_instance.main.endpoint
  sensitive = true       # hidden in CLI output; still in state
}

output "service_urls" {
  value = { for k, s in local.public_services : k => "https://${k}.${var.domain}" }
}
```

`terraform output -json` makes them consumable by scripts and CI (e.g. pass the ALB name to a deploy job).

## 5. Useful Patterns

- **Environment maps**: `lookup(local.sizes, var.environment)` with `local.sizes = { dev = "t4g.small", prod = "m7g.large" }`.
- **Feature toggles**: `count = var.enable_bastion ? 1 : 0` (and reference with `one(aws_instance.bastion[*].id)`).
- **Preconditions/postconditions** on resources and outputs to assert assumptions:

```hcl
resource "aws_instance" "web" {
  # ...
  lifecycle {
    precondition {
      condition     = data.aws_ami.app.architecture == "arm64"
      error_message = "The selected AMI must be arm64 for Graviton instances."
    }
  }
}
```

## Under the Hood: Evaluation and Unknown Values

Terraform evaluates expressions while walking the dependency graph. Variables are known at the start; locals and resource attributes become known as their dependencies are evaluated. Some values are only **known after apply** (a generated ID, an IP assigned by AWS). In a plan these propagate as *unknown*: `"arn:aws:s3:::${aws_s3_bucket.x.id}"` is unknown if the bucket doesn't exist yet. Unknowns matter in two places: `count`/`for_each` must be known at plan time (Terraform must know how many instances to plan — "Invalid for_each argument… depends on resource attributes that cannot be determined until apply"), and provider configuration can't depend on unknown values. The fix is to drive `count`/`for_each` from variables or known data (e.g. a map of names) rather than from attributes of resources created in the same run.

![Values flow from defaults, environment variables, tfvars files and -var flags into typed, validated variables; locals derive names, tags and filtered maps; resources consume them; outputs expose results to the CLI, parent modules and other configurations](diagrams/01-values-flow.svg)

## Common Pitfalls

1. **Untyped `any` variables** — errors surface deep inside resources.
2. **Secrets in tfvars committed to Git**.
3. **Believing `sensitive` encrypts state**.
4. **`for_each` over values unknown until apply**.
5. **Variables for things callers should never change** — use locals.
6. **No descriptions** — modules become guesswork for users.

## Quick Reference

```
variable { type, default, description, validation, sensitive, nullable } · optional(T, default) in objects
precedence: default < TF_VAR_ < terraform.tfvars < *.auto.tfvars < -var-file < -var
locals { name_prefix, tags, derived maps } · output { value, description, sensitive } · terraform output -json
precondition / postcondition for assumptions · count/for_each must be known at plan time
