How to Concatenate Lists in Terraform Using concat()
When working with lists in Terraform, you'll often need to combine multiple lists into a single list. The concat() function is built specifically for this purpose. It takes two or more lists as arguments and returns a new list containing all the elements in order.
This is useful when you're combining configuration from multiple sources, merging subnet IDs from different availability zones, or building resource lists dynamically.
TLDR: Use concat(list1, list2, ...) to combine multiple lists in Terraform. It returns a new list containing all elements from the input lists in order. You can concatenate any number of lists, and it works with lists of strings, numbers, or objects. For more complex merging with deduplication, use distinct(concat(...)). To merge maps instead of lists, use merge().
Basic List Concatenation
The simplest use of concat() combines two lists:
locals {
list_a = ["item1", "item2"]
list_b = ["item3", "item4"]
combined = concat(local.list_a, local.list_b)
# Result: ["item1", "item2", "item3", "item4"]
}
output "combined_list" {
value = local.combined
}
The order matters - elements from the first list appear first, followed by elements from the second list, and so on.
Concatenating Multiple Lists
You can pass more than two lists to concat():
locals {
public_subnets = ["subnet-111", "subnet-222"]
private_subnets = ["subnet-333", "subnet-444"]
database_subnets = ["subnet-555", "subnet-666"]
all_subnets = concat(
local.public_subnets,
local.private_subnets,
local.database_subnets
)
# Result: ["subnet-111", "subnet-222", "subnet-333", "subnet-444", "subnet-555", "subnet-666"]
}
resource "aws_security_group_rule" "allow_from_all_subnets" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [for subnet in local.all_subnets : data.aws_subnet.subnets[subnet].cidr_block]
security_group_id = aws_security_group.app.id
}
This pattern is helpful when you have subnets organized by tier but need to reference all of them together.
Combining Variable Lists With Fixed Values
You can mix variable lists with hardcoded lists:
variable "additional_security_groups" {
type = list(string)
default = []
}
locals {
# Always include the default security group, plus any additional ones
security_groups = concat(
[aws_security_group.default.id],
var.additional_security_groups
)
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = "t3.medium"
vpc_security_group_ids = local.security_groups
}
This ensures every instance has the default security group while still allowing additional groups to be specified.
Concatenating Lists of Objects
concat() works with lists of any type, including objects:
locals {
app_containers = [
{
name = "web"
image = "nginx:latest"
port = 80
},
{
name = "api"
image = "myapp/api:v1"
port = 8080
}
]
sidecar_containers = [
{
name = "logging"
image = "fluent/fluent-bit:latest"
port = 24224
}
]
all_containers = concat(local.app_containers, local.sidecar_containers)
# Result: [{name="web",...}, {name="api",...}, {name="logging",...}]
}
resource "aws_ecs_task_definition" "app" {
family = "my-app"
container_definitions = jsonencode([
for container in local.all_containers : {
name = container.name
image = container.image
portMappings = [{
containerPort = container.port
}]
}
])
}
This pattern is useful for adding sidecar containers to your task definitions without duplicating the main container configurations.
Removing Duplicates After Concatenation
If your lists might contain duplicate values, use distinct() after concatenating:
locals {
dev_users = ["[email protected]", "[email protected]", "[email protected]"]
prod_users = ["[email protected]", "[email protected]"]
# Without distinct - has duplicates
all_users_with_dupes = concat(local.dev_users, local.prod_users)
# Result: ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]
# With distinct - duplicates removed
all_users = distinct(concat(local.dev_users, local.prod_users))
# Result: ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]
}
resource "aws_iam_group_membership" "developers" {
name = "developers"
users = local.all_users
group = aws_iam_group.developers.name
}
The distinct() function removes duplicate values, keeping only the first occurrence of each unique value.
Conditional Concatenation
Sometimes you only want to concatenate lists when certain conditions are met:
variable "environment" {
type = string
}
variable "enable_monitoring" {
type = bool
default = false
}
locals {
base_security_groups = [
aws_security_group.app.id,
aws_security_group.database.id
]
monitoring_security_groups = [
aws_security_group.prometheus.id,
aws_security_group.grafana.id
]
prod_security_groups = [
aws_security_group.waf.id
]
# Build the list conditionally
security_groups = concat(
local.base_security_groups,
var.enable_monitoring ? local.monitoring_security_groups : [],
var.environment == "production" ? local.prod_security_groups : []
)
}
The ternary operator condition ? true_value : false_value returns an empty list when the condition is false, which concat() handles gracefully.
Flattening Nested Lists
When you have a list of lists and want a single flat list, combine concat() with the splat operator or flatten():
locals {
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
# Create subnets per AZ
subnets_by_az = [
for az in local.availability_zones : [
"subnet-public-${az}",
"subnet-private-${az}"
]
]
# Result: [["subnet-public-us-east-1a", "subnet-private-us-east-1a"], [...], [...]]
# Flatten to a single list
all_subnet_ids = flatten(local.subnets_by_az)
# Result: ["subnet-public-us-east-1a", "subnet-private-us-east-1a", "subnet-public-us-east-1b", ...]
}
While this example uses flatten(), you could also use concat() with the splat operator:
locals {
# Alternative approach with concat
all_subnet_ids = concat(local.subnets_by_az...)
}
The ... (splat) operator unpacks the list of lists into separate arguments for concat().
Concatenating Output From Multiple Modules
A common use case is combining outputs from multiple modules:
module "vpc_us_east_1" {
source = "./modules/vpc"
region = "us-east-1"
cidr = "10.0.0.0/16"
}
module "vpc_us_west_2" {
source = "./modules/vpc"
region = "us-west-2"
cidr = "10.1.0.0/16"
}
locals {
# Combine subnet IDs from both regions
all_private_subnets = concat(
module.vpc_us_east_1.private_subnet_ids,
module.vpc_us_west_2.private_subnet_ids
)
all_public_subnets = concat(
module.vpc_us_east_1.public_subnet_ids,
module.vpc_us_west_2.public_subnet_ids
)
}
output "all_subnets" {
value = concat(local.all_private_subnets, local.all_public_subnets)
}
This lets you work with resources across multiple regions or modules uniformly.
Building Lists Dynamically With for Expressions
Combine concat() with for expressions for more complex list building:
variable "services" {
type = map(object({
port = number
additional_ports = list(number)
}))
default = {
web = {
port = 80
additional_ports = [443, 8080]
}
api = {
port = 3000
additional_ports = [3001]
}
}
}
locals {
# Build a flat list of all ports across all services
all_ports = distinct(flatten([
for service_name, service in var.services : concat(
[service.port],
service.additional_ports
)
]))
# Result: [80, 443, 8080, 3000, 3001]
}
resource "aws_security_group_rule" "allow_all_service_ports" {
count = length(local.all_ports)
type = "ingress"
from_port = local.all_ports[count.index]
to_port = local.all_ports[count.index]
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.services.id
}
This pattern extracts all ports from a complex configuration structure into a simple flat list.
Concatenating With Empty Lists
concat() handles empty lists gracefully - they simply don't contribute any elements:
locals {
list_a = ["a", "b"]
list_b = []
list_c = ["c"]
result = concat(local.list_a, local.list_b, local.list_c)
# Result: ["a", "b", "c"]
}
This makes it safe to use conditional expressions that might return empty lists.
Comparing concat() With Other Functions
concat() vs merge():
concat()is for lists:concat(["a"], ["b"])→["a", "b"]merge()is for maps:merge({a=1}, {b=2})→{a=1, b=2}
concat() vs flatten():
concat()joins multiple lists:concat(["a"], ["b"])→["a", "b"]flatten()unpacks nested lists:flatten([["a"], ["b"]])→["a", "b"]
concat() vs union():
concat()keeps duplicates:concat(["a", "b"], ["b", "c"])→["a", "b", "b", "c"]union()removes duplicates:union(["a", "b"], ["b", "c"])→["a", "b", "c"]
Actually, union() is the same as distinct(concat(...)):
locals {
list_a = ["a", "b"]
list_b = ["b", "c"]
# These are equivalent
using_distinct = distinct(concat(local.list_a, local.list_b))
using_union = union(local.list_a, local.list_b)
# Both result in: ["a", "b", "c"]
}
Practical Example: Combining Tags
A common pattern is merging tags from multiple sources:
variable "common_tags" {
type = map(string)
default = {
ManagedBy = "terraform"
Project = "my-app"
}
}
variable "environment_tags" {
type = map(string)
default = {
Environment = "production"
CostCenter = "engineering"
}
}
locals {
# Note: This uses merge() for maps, not concat() for lists
all_tags = merge(
var.common_tags,
var.environment_tags
)
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = "t3.medium"
tags = local.all_tags
}
Wait, that example uses merge() for maps. For lists of tags (less common), you'd use concat():
locals {
base_tag_specs = [
{
resource_type = "instance"
tags = {
Name = "app-server"
}
}
]
volume_tag_specs = [
{
resource_type = "volume"
tags = {
Name = "app-volume"
}
}
]
all_tag_specifications = concat(
local.base_tag_specs,
local.volume_tag_specs
)
}
resource "aws_launch_template" "app" {
name = "app-template"
dynamic "tag_specifications" {
for_each = local.all_tag_specifications
content {
resource_type = tag_specifications.value.resource_type
tags = tag_specifications.value.tags
}
}
}
Error Handling
concat() requires all arguments to be lists. Passing a non-list value causes an error:
locals {
# ERROR: concat() requires all arguments to be lists
invalid = concat(["a", "b"], "c")
}
If you have a single value you want to add to a list, wrap it in brackets:
locals {
# Correct: wrap the string in brackets to make it a list
valid = concat(["a", "b"], ["c"])
# Result: ["a", "b", "c"]
}
The concat() function is straightforward but essential for building dynamic infrastructure configurations. Use it whenever you need to combine lists from multiple sources, and combine it with distinct(), flatten(), or conditional expressions for more advanced list manipulation.
Found an issue?