Complex Variable Types: Objects, Tuples and Optional Attributes
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.
- 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 destroyApply 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]
