In today’s blog post, we will learn maps in terraform. In particular, how to define a map, how access a map elements, getting specific map elements, merging two maps data, different terraform functions related to maps, and best practices. So without any further delay, let us get started!
What are Maps?
In Terraform, a map
is a collection type that stores a set of unique string keys, each associated with a value of any type. It is used to group related values and access them by a descriptive name. Maps/objects are represented by a pair of curly braces containing a series of <KEY> = <VALUE>
pairs. The Key/value pairs can be separated by either a comma or a line break. The keys in a map must be strings; they can be left unquoted if they are a valid identifier, but must be quoted otherwise.
Defining Maps and Objects
Maps and objects in Terraform both represent collections of named attributes. The distinction often lies in the rigidity of their type constraints:
- Map (or
map
type): A collection where all values are expected to be of the same type. It is an unordered collection of key-value pairs, where keys are strings. - Object (or
object
type): A fixed set of named attributes, where each attribute can have a different type. Objects are used when you need a well-defined structure for heterogeneous data.
For simplicity, the term “map” is often used broadly to refer to key-value collections, but understanding the object
type is also important.
Defining a Map
You define a map by enclosing its key-value pairs in curly braces {}
, where keys are strings (often unquoted if they are simple identifiers) and values can be any Terraform type.
# main.tf
# Define a simple map of string to string (common for tags)
variable "common_tags" {
description = "Common tags for resources."
type = map(string)
default = {
Environment = "dev"
Project = "Infra"
}
}
# Define a map with mixed value types (implicitly becomes map(any))
output "resource_settings" {
value = {
instance_type = "t2.micro"
min_capacity = 1
enabled = true
}
# Outcome: { "instance_type" = "t2.micro", "min_capacity" = 1, "enabled" = true }
}
Explanation:
- The
common_tags
variable is explicitly typed asmap(string)
, ensuring all values are strings. This is a common pattern for defining resource tags. - The
resource_settings
output demonstrates a map where values have different types. Terraform infers the type asmap(any)
if not explicitly constrained, allowing flexibility.
Defining an Object
The object
type is used when you need a specific, fixed structure where each attribute has a predefined type. This provides strong type checking and clarity for complex data structures.
# main.tf
# Define a variable with an object type constraint
variable "service_endpoint" {
description = "Configuration for a service endpoint."
type = object({
url = string
port = number
is_secure = bool
})
default = {
url = "api.example.com"
port = 443
is_secure = true
}
}
output "endpoint_url" {
value = var.service_endpoint.url
# Outcome: "api.example.com"
}
Explanation:
- The
service_endpoint
variable is defined as anobject
with specific attribute names (url
,port
,is_secure
) and their corresponding types. This ensures data passed to this variable strictly adheres to the defined schema. - Object attributes are accessed using dot notation.
Accessing Map and Object Elements
You can retrieve values from maps and objects using dot notation or bracket notation. For safer access, especially when a key might be missing, the lookup
and try
functions are recommended.
Dot and Bracket Notation
# main.tf
variable "instance_specs" {
type = map(string)
default = {
cpu = "2 cores"
memory = "4GB"
disk = "100GB SSD"
}
}
# Accessing map elements using dot notation (for simple keys)
output "instance_cpu_dot" {
description = "CPU specification using dot notation."
value = var.instance_specs.cpu
# Outcome: "2 cores"
}
# Accessing map elements using bracket notation (for any key, including dynamic or with special characters)
output "instance_memory_bracket" {
description = "Memory specification using bracket notation."
value = var.instance_specs["memory"]
# Outcome: "4GB"
}
# Example with a key that might contain special characters or be dynamic
output "tag_with_hyphen" {
value = {
"my-tag-key" = "my-value"
}["my-tag-key"]
# Outcome: "my-value"
}
Explanation:
- Dot notation (
.key
) is clean and preferred for simple, static keys. - Bracket notation (
["key"]
) is necessary for keys that are not valid identifiers (e.g., contain hyphens, start with numbers) or when the key itself is a variable or expression.
Safe Access with lookup
The lookup
function allows you to retrieve a value from a map by key, providing a default value to return if the key does not exist. This prevents runtime errors.
# main.tf
output "lookup_disk_spec" {
description = "Disk specification, with a default if not found."
value = lookup(var.instance_specs, "disk", "200GB HDD")
# Outcome: "100GB SSD" (since 'disk' exists)
}
output "lookup_network_spec" {
description = "Network specification, which doesn't exist so default is used."
value = lookup(var.instance_specs, "network", "Gigabit Ethernet")
# Outcome: "Gigabit Ethernet" (since 'network' does not exist)
}
Explanation:
lookup
is important for creating configurations that can gracefully handle missing optional attributes, falling back to sensible defaults.
Error Handling with try
The try
function evaluates a sequence of arguments and returns the result of the first expression that succeeds without producing an error. This is useful for deeply nested or optional attributes.
# main.tf
variable "app_config" {
type = object({
database = optional(object({
host = string
port = number
}))
cache = optional(object({
enabled = bool
}))
})
default = {
database = { host = "db.example.com", port = 5432 }
# cache attribute is missing in default
}
}
output "database_host_try" {
description = "Database host using try for safe access."
value = try(var.app_config.database.host, "default-db-host")
# Outcome: "db.example.com"
}
output "cache_enabled_try" {
description = "Cache enabled status using try for safe access."
value = try(var.app_config.cache.enabled, false)
# Outcome: false (since 'cache' is optional and not present in default)
}
Explanation of the code:
try(var.app_config.database.host, "default-db-host")
– ifvar.app_config.database.host
is not defined thetry
function will returndefault-db-host
try(var.app_config.cache.enabled, false)
– ifvar.app_config.cache.enabled
is not defined or set, the try function will returnfalse
Common Map Functions
Terraform offers specific functions for common map operations.
Getting Keys and Values
The keys
function returns a list of all keys in a map, and the values
function returns a list of all values.
# main.tf
output "instance_spec_keys" {
description = "List of all keys in instance_specs."
value = keys(var.instance_specs)
# Outcome: ["cpu", "memory", "disk"] (order not guaranteed)
}
output "instance_spec_values" {
description = "List of all values in instance_specs."
value = values(var.instance_specs)
# Outcome: ["2 cores", "4GB", "100GB SSD"] (order corresponds to keys, but overall map order not guaranteed)
}
Explanation:
keys
andvalues
are useful for iterating over map entries, especially when combined withfor
expressions or for logging/debugging purposes.
Merging Maps
The merge
function combines two or more maps. If a key exists in multiple maps, the value from the rightmost map takes precedence.
# main.tf
variable "base_config" {
type = map(string)
default = {
region = "us-east-1"
env = "dev"
}
}
variable "override_config" {
type = map(string)
default = {
env = "prod"
size = "large"
}
}
output "merged_full_config" {
description = "Merged configuration, with overrides."
value = merge(var.base_config, var.override_config, {"owner" = "platform-team"})
# Outcome: { "region" = "us-east-1", "env" = "prod", "size" = "large", "owner" = "platform-team" }
}
Explanation:
merge
is extremely useful for building composite configurations, combining default settings with environment-specific overrides, or stacking configurations from multiple modules.
Transforming and Filtering with For Expressions
The for
expression is incredibly powerful for manipulating maps. You can iterate over map entries, transform keys or values, filter entries, and create new maps or lists.
Transforming Map Values
# main.tf
variable "instance_sizes" {
type = map(string)
default = {
dev = "t2.micro"
prod = "t3.medium"
}
}
output "instance_amis" {
description = "Map of instance types to specific AMIs."
value = {
for env, size in var.instance_sizes :
env => "${size}-ami-latest" # Transform value
}
# Outcome: { "dev" = "t2.micro-ami-latest", "prod" = "t3.medium-ami-latest" }
}
Explanation:
- This
for
expression iterates over each key-value pair (env
,size
) ininstance_sizes
and creates a new map where the keys are the same, but the values are transformed strings.
Filtering Map Entries
# main.tf
variable "security_rules" {
type = map(string)
default = {
http = "80"
https = "443"
ssh = "22"
ftp = "21"
}
}
output "common_ports" {
description = "Map containing only common HTTP/HTTPS/SSH ports."
value = {
for name, port in var.security_rules : name => port
if contains(["80", "443", "22"], port) # Filter based on value
}
# Outcome: { "http" = "80", "https" = "443", "ssh" = "22" }
}
Explanation:
- This
for
expression iterates oversecurity_rules
and includes key-value pairs in the new map only if theport
value is one of the specified common ports.
Practical Use Cases
for_each
with Resource Creation: Maps are the ideal input forfor_each
meta-argument, allowing you to create multiple instances of a resource where each instance is uniquely identified by a map key and configured by its corresponding value. This is superior tocount
for managing individual resource lifecycles.- Dynamic Tags: Define all your resource tags as a map and then use
merge
to combine global tags with resource-specific tags, passing the resulting map to thetags
argument of resources. - Complex Nested Configurations: Represent complex configurations, like network interfaces with multiple IP addresses, as objects within maps or nested maps, allowing for structured and readable data.
- Module Configuration: Define configurable options for your modules as map or object variables, making modules highly reusable and adaptable to different scenarios.
Best Practices for Map and Object Manipulation
- Choose Wisely: Use maps/objects when you need named attributes and associative access (accessing by key). Use lists when order matters and you need numerically indexed access. Use sets when only uniqueness matters and order is irrelevant.
- Using
for_each
: For creating multiple resources where each needs a unique identity and potentially different attributes, usefor_each
with a map. This greatly simplifies managing resource lifecycles compared tocount
. - Handle Missing Keys: Always assume keys might be missing if inputs are dynamic. Use
lookup
with default values ortry
expressions to prevent runtime errors. - Readability: For very complex map manipulations, break them down into smaller
local
values. This improves clarity and debugging. - Schema Definition with
object
: For module inputs or complex data structures, explicitly defineobject
types for variables to enforce schema, improve validation, and provide better documentation. - Keys as Identifiers: When using maps for
for_each
, ensure your keys are unique and stable identifiers for the resources you are creating.
Conclusion
Mastering map and object manipulation is an important skill required for almost everyday in a terraform development lifecycle . By effectively leveraging map definitions, attribute access methods like dot/bracket notation and lookup
/try
, and the powerful for
expressions for transformation and filtering, you gain great flexibility in your infrastructure definitions. These capabilities are required for handling dynamic configurations, managing resource attributes, and using for_each
based resource provisioning, ultimately leading to more modular, scalable, and maintainable Infrastructure as Code.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com