Heading to KubeCon North America 2025?

Meet with Spacelift 🚀

How to Use Terraform List of Objects [Examples]

terraform

🚀 Level Up Your Infrastructure Skills

You focus on building. We’ll keep you updated. Get curated infrastructure insights that help you make smarter decisions.

Working with multiple resources in Terraform often calls for clean, organized, and flexible configurations. Grouping related data in a structured way helps manage repeating sets of values, like server definitions or security rules.

In this article, we’ll cover the Terraform list of object constructs and how to use it in different scenarios.

What is Terraform's list of objects?

A Terraform list of objects is a collection data type that allows you to group related sets of values (objects) into an ordered sequence (list). Each object in the list can contain multiple named attributes, allowing you to model complex infrastructure configurations in a clean and scalable way.

This is useful when managing resources with a similar structure but varying values. Lists of objects are especially helpful for defining multiple similar resources with different parameters in modules or resource blocks.

Example 1: Creating multiple AWS S3 buckets with tags

In this example, a variable s3_buckets holds a list of objects. Each object contains the name of an S3 bucket and its environment tag.

Let’s use a for_each loop to iterate over this list and create an S3 bucket for each entry.

variable "s3_buckets" {
  type = list(object({
    name = string
    environment = string
  }))
  default = [
    {
      name        = "dev-bucket"
      environment = "dev"
    },
    {
      name        = "prod-bucket"
      environment = "prod"
    }
  ]
}

resource "aws_s3_bucket" "example" {
  for_each = { for idx, bucket in var.s3_buckets : bucket.name => bucket }

  bucket = each.value.name

  tags = {
    Environment = each.value.environment
    Name        = each.value.name
  }
}

Note: S3 bucket names must be globally unique across all AWS accounts/Regions. Also, many S3 settings now live in separate resources (e.g., aws_s3_bucket_versioning, aws_s3_bucket_website_configuration).

We used a map in for_each so each object is keyed by its name (bucket.name => bucket), making it easier to reference individual buckets. Inside the resource block, each.value provides access to the individual object’s attributes (name and environment). This method avoids repetitive code and centralizes configuration, making it easier to scale and maintain.

Example 2: Creating multiple Route 53 DNS records

In this scenario, we’re provisioning multiple DNS records in AWS Route 53 using a single aws_route53_record resource. The input variable dns_records holds a list of objects, each representing a DNS entry with the following fields:

  • name: the subdomain (e.g., www, api)
  • type: the DNS record type (e.g., A, CNAME)
  • ttl: the time-to-live value
  • records: a list of IPs or domains the record points to
variable "dns_records" {
  type = list(object({
    name    = string
    type    = string
    ttl     = number
    records = list(string)
  }))
  default = [
    {
      name    = "www"
      type    = "A"
      ttl     = 300
      records = ["192.0.2.1"]
    },
    {
      name    = "api"
      type    = "CNAME"
      ttl     = 300
      records = ["example.com"]
    }
  ]
}

resource "aws_route53_record" "this" {
  for_each = { for record in var.dns_records : record.name => record }

  zone_id = "Z3P5QSUBK4POTI"  # Replace with your actual hosted zone ID
  name    = "${each.key}.example.com"
  type    = each.value.type
  ttl     = each.value.ttl
  records = each.value.records
}

We then use for_each to iterate over this list. Each object is keyed by its name so Terraform can manage each DNS record distinctly. For example, www.example.com points to an IP address, and api.example.com is a CNAME pointing to another domain.

This example is perfect for managing a growing number of DNS entries without duplicating code. As your app evolves and new services are added (e.g., auth, blog, docs), you can simply update the dns_records list without touching the resource definition itself.

Use case example: Deploying multiple EC2 instances with custom settings

Imagine you’re working for a company that needs to spin up a set of EC2 instances in AWS to serve different purposes — like a web server, a database server, and a logging server. Each instance has different instance types, tags, and AMIs, but they all live in the same VPC and availability zone.

Traditionally, you might write separate aws_instance blocks for each server, repeating a lot of configuration. But using a list of objects makes it clean, scalable, and DRY (Don’t Repeat Yourself).

variable "ec2_instances" {
  description = "List of EC2 instances to create"
  type = list(object({
    name         = string
    instance_type = string
    ami          = string
  }))
  default = [
    {
      name          = "web-server"
      instance_type = "t2.micro"
      ami           = "ami-0abcdef1234567890"  # Placeholder AMI
    },
    {
      name          = "db-server"
      instance_type = "t3.medium"
      ami           = "ami-0abcdef1234567890"
    },
    {
      name          = "log-server"
      instance_type = "t3.small"
      ami           = "ami-0abcdef1234567890"
    }
  ]
}

resource "aws_instance" "servers" {
  for_each = { for inst in var.ec2_instances : inst.name => inst }

  ami           = each.value.ami
  instance_type = each.value.instance_type
  tags = {
    Name = each.value.name
    Role = each.value.name
  }
}

This configuration starts by defining a variable called ec2_instances, which holds a list of objects. Each object represents one EC2 instance and defines its name, instance_type, and ami.

Instead of writing three separate aws_instance resource blocks, we use for_each with a derived map where each object is keyed by its name. This ensures that each instance is uniquely identified and can be created or destroyed independently.

Within the aws_instance block, each.value gives us access to the current object’s data, its AMI, instance type, and name. The tags section uses this data to label the instance appropriately.

Key points

Terraform’s list of objects construct helps manage complex infrastructure definitions more effectively by:

  • Enabling parametrization of resources
  • Supporting reusability through modules
  • Reducing code duplication
  • Making configurations clearer and easier to maintain 

Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:

  • Policies (based on Open Policy Agent)
  • Multi-IaC workflows
  • Self-service infrastructure
  • Integrations with any third-party tools

If you want to learn more about Spacelift, create a free account today or book a demo with one of our engineers.

Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.

Automate Terraform deployments with Spacelift

Automate your infrastructure provisioning, and build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and many more.

Learn more

Terraform Commands Cheat Sheet

Grab our ultimate cheat sheet PDF
for all the Terraform commands
and concepts you need.

Share your data and download the cheat sheet