TFLAB03-Complex Variable Types: Objects, Tuples and Optional Attributes

Complex Variable Types: Objects, Tuples and Optional Attributes

🔧 Terraform Core ⭐ Beginner object tuple optional() nullable Complex Types

Scenario

Your module accepts a bunch of related settings — bucket name, environment, whether to enable versioning, and a set of tags. Right now these are five separate variables, and users keep forgetting to set one or two of them. You want to group related settings into a single object variable so they're passed as one logical unit.

You also need to understand tuple for heterogeneous ordered lists, optional() for attributes that can be omitted, and nullable for variables that accept null.

Your Objectives
  • Declare an object variable called bucket_config with attributes: name (string), environment (string), versioning (bool), tags (map of strings).
  • Make tags optional with a default empty map using optional(map(string), {}).
  • Declare a tuple variable that accepts a [string, number, bool] triple.
  • Declare a nullable variable of type string — pass null and observe the behavior.
  • Use all three variables in an S3 bucket resource and output the resolved values.

Additional Context

object defines a structured type with named attributes and specific types for each. Think of it as a mini-schema. tuple is an ordered list where each position has its own type — unlike list, which requires all elements to share the same type.

optional() was introduced in Terraform 1.3. It lets you mark individual attributes in an object as optional with an optional default. Without it, every attribute is required and must be supplied by the caller — which makes complex objects annoying to use.

nullable = false (the non-default setting) prevents callers from passing null. By default, all variables are nullable — meaning null bypasses the default value entirely.

variables.tf

# variables.tf

# OBJECT — a structured type with named, typed attributes.
variable "bucket_config" {
  description = "Configuration for the S3 bucket as a single object."
  type = object({
    name        = string
    environment = string
    versioning  = bool
    # optional() with a default: if the caller omits 'tags', it defaults to {}
    tags        = optional(map(string), {})
  })

  default = {
    name        = "my-app-data"
    environment = "dev"
    versioning  = true
    # tags is optional — we can omit it here, it defaults to {}
  }
}

# TUPLE — an ordered list with a specific type at each position.
# Position 0 = string (label), Position 1 = number (priority), Position 2 = bool (active)
variable "deployment_info" {
  description = "A tuple: [label, priority, active]."
  type        = tuple([string, number, bool])
  default     = ["primary", 1, true]
}

# NULLABLE — a variable that explicitly accepts null.
# By default all variables are nullable. Setting nullable = false rejects null.
variable "override_prefix" {
  description = "Optional prefix override. Pass null to use the default naming."
  type        = string
  default     = null    # default IS null — no prefix unless explicitly set
  nullable    = true    # this is the default, shown here for clarity
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

main.tf

# main.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" { region = var.aws_region }

locals {
  # If override_prefix is null, use the environment from the object.
  # coalesce() returns the first non-null, non-empty value.
  prefix = var.override_prefix != null ? var.override_prefix : var.bucket_config.environment
}

resource "aws_s3_bucket" "app" {
  bucket = "${local.prefix}-${var.bucket_config.name}"

  tags = merge(var.bucket_config.tags, {
    Name        = var.bucket_config.name
    Environment = var.bucket_config.environment
    ManagedBy   = "terraform"
    DeployLabel = var.deployment_info[0]   # tuple element by index
    Priority    = tostring(var.deployment_info[1])
    Active      = tostring(var.deployment_info[2])
  })
}

resource "aws_s3_bucket_versioning" "app" {
  bucket = aws_s3_bucket.app.id
  versioning_configuration {
    status = var.bucket_config.versioning ? "Enabled" : "Suspended"
  }
}

outputs.tf

output "bucket_name"      { value = aws_s3_bucket.app.id }
output "resolved_prefix"  { value = local.prefix }
output "object_values"    { value = var.bucket_config }
output "tuple_values"     { value = var.deployment_info }
output "override_is_null" { value = var.override_prefix == null }

Workflow Commands

terraform init && terraform plan

# Apply with defaults (override_prefix is null → uses "dev")
terraform apply

# Supply a custom object — omitting 'tags' (it's optional, defaults to {})
terraform apply -var='bucket_config={"name":"analytics","environment":"prod","versioning":false}'

# Supply an override prefix (not null anymore)
terraform apply -var="override_prefix=custom"

terraform destroy
✓ Expected Output After Apply (defaults)
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

bucket_name      = "dev-my-app-data"
override_is_null = true
resolved_prefix  = "dev"
object_values    = {
  "environment" = "dev"
  "name"        = "my-app-data"
  "tags"        = {}
  "versioning"  = true
}
tuple_values     = ["primary", 1, true]