Skip to main content
A list gives you elements. An object gives you key-value pairs. When each resource in a set needs its own name and its own configuration value — buckets with different roles, SSM parameters with different names and values, security group rules with different ports — for_each over an object is the right tool. Both each.key (the map key) and each.value (the map value) are available inside the block, giving you access to both dimensions of each entry. Pulumi keys resources in state by each.key, which makes renames and removals predictable: changing a key destroys and recreates that resource, while changing only the value updates it in place.

What you’ll learn

  • How for_each over an object provides both each.key and each.value
  • The flat string map pattern — the most common form
  • How Pulumi uses each.key to track resources in state

Why this matters

Objects give you named, independently-addressable resources. Each each.key becomes a distinct Pulumi resource identity in state — you can remove one entry from the map without affecting the others, unlike list-based for_each where index shifts can cause unexpected destroys.

The source code

# for_each over an object — each.key is the map key, each.value is the value.
unit "aws_s3_bucket_v2" "bucket" {
  for_each = {
    assets  = "primary"
    logs    = "archive"
    uploads = "transient"
  }

  bucket = "${input.env}-${each.key}"

  tags = {
    Name = "${input.env}-${each.key}"
    Env  = input.env
    Role = each.value
  }
}

# Flat string map — SSM parameters keyed by name, valued by content.
unit "aws_ssm_parameter" "config" {
  for_each = {
    log_level = "INFO"
    region    = "us-east-1"
    max_conns = "100"
  }

  name  = "/${input.env}/app/${each.key}"
  type  = "String"
  value = each.value
}

How it works

1

The object is resolved at compile time

The object literal is Resolved<T> — all keys and values are known at compile time. ubx expands it into one IRUnit node per entry in the IR pass.
2

each.key and each.value are substituted per entry

For the buckets block: first iteration gives each.key = "assets", each.value = "primary"; second gives "logs" / "archive"; and so on. Both are available anywhere inside the block body.
3

Pulumi keys by each.key

The generated code creates one resource per map entry, keyed by each.key in Pulumi state. Removing "uploads" from the map on the next apply destroys only that bucket — "assets" and "logs" are unaffected.

What ubx generates

// Generated by ubx — do not edit
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const env = config.get("env") || "dev";

const bucketMap: Record<string, string> = {
    assets: "primary", logs: "archive", uploads: "transient",
};
const buckets = Object.fromEntries(
    Object.entries(bucketMap).map(([key, value]) => [key,
        new aws.s3.BucketV2(`bucket-${key}`, {
            bucket: `${env}-${key}`,
            tags: { Name: `${env}-${key}`, Env: env, Role: value },
        })
    ])
);

const ssmMap: Record<string, string> = {
    log_level: "INFO", region: "us-east-1", max_conns: "100",
};
const ssmParams = Object.fromEntries(
    Object.entries(ssmMap).map(([key, value]) => [key,
        new aws.ssm.Parameter(`config-${key}`, {
            name: `/${env}/app/${key}`,
            type: "String",
            value,
        })
    ])
);

export const envOut = env;

Common mistakes

ubx v1 does not support nested field access on each.value — patterns like each.value.port are not yet supported. Use flat string maps where each value is a primitive. If you need structured per-entry configs, use separate unit blocks with when conditions, or a component.
Object keys become part of the Pulumi resource name in state. Keep them lowercase, hyphen-safe strings. Avoid keys that look like resource addresses (aws_s3_bucket) — keep them short and descriptive (assets, logs).

Run it

ubx validate
ubx plan             # 3 buckets + 3 SSM parameters
ubx plan --var env=prod

What you learned

for_each over an object provides each.key (the map key) and each.value (the map value)
Pulumi keys resources by each.key — removing an entry destroys only that resource
Flat string maps are the most portable pattern in ubx v1

Next steps

count meta-argument

Create N copies of a resource by number

when conditional resource

Conditionally create or skip a resource entirely