Skip to main content
Some Pulumi resources accept a repeated nested block — security group rules, EBS volume attachments, listener rules on a load balancer, IAM policy statements. In raw Terraform or Pulumi SDK code, you’d either write one block per rule (tedious and brittle) or use a dynamic block to generate them from a list. ubx supports the same pattern: the dynamic keyword takes a collection and a content sub-block, and generates one instance of the nested block per element in the collection. The current element is available as <block_name>.value inside content, giving you access to each entry’s attributes. The result is a single declaration that generates as many nested blocks as the collection has entries — and adding a new entry is as simple as adding a new object to the list.

What you’ll learn

  • How dynamic generates repeated nested blocks from a list
  • How to access the current element with <block_name>.value
  • When to use dynamic vs separate unit blocks

Why this matters

Without dynamic, adding a new port to a security group means adding an entire ingress block by hand. With dynamic, you add one object to a list. The security group’s rule configuration becomes data — easy to read, easy to audit, easy to extend.

The source code

# dynamic blocks expand a nested block once per element in a collection.
# Here, each port object in the list becomes one ingress rule.
unit "aws_security_group" "app" {
  name   = "${input.env}-app-sg"
  vpc_id = "vpc-placeholder"

  dynamic "ingress" {
    for_each = [
      { port = 80,   protocol = "tcp", description = "HTTP" },
      { port = 443,  protocol = "tcp", description = "HTTPS" },
      { port = 8080, protocol = "tcp", description = "App port" },
    ]

    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      description = ingress.value.description
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

  dynamic "egress" {
    for_each = [{ port = 0, protocol = "-1", cidr = "0.0.0.0/0" }]

    content {
      from_port   = egress.value.port
      to_port     = egress.value.port
      protocol    = egress.value.protocol
      cidr_blocks = [egress.value.cidr]
    }
  }

  tags = {
    Name = "${input.env}-app-sg"
  }
}

How it works

1

dynamic iterates over the for_each collection

The list of objects is Resolved<T> — known at compile time. ubx iterates over it in the IR pass and generates one nested block per element, substituting ingress.value.port, ingress.value.protocol, etc. for each entry.
2

content defines the shape of each generated block

The content sub-block is the template. Every attribute inside it is evaluated once per collection element, with <block_name>.value referring to the current element’s fields.
3

Generated code expands to explicit nested blocks

The emitter expands each dynamic into concrete property objects. The generated TypeScript/Python/Go contains no loops for the nested blocks — each rule is a separate literal object in the resource constructor.

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 app = new aws.ec2.SecurityGroup("app", {
    name: `${env}-app-sg`,
    vpcId: "vpc-placeholder",
    ingress: [
        { fromPort: 80,   toPort: 80,   protocol: "tcp", description: "HTTP",      cidrBlocks: ["0.0.0.0/0"] },
        { fromPort: 443,  toPort: 443,  protocol: "tcp", description: "HTTPS",     cidrBlocks: ["0.0.0.0/0"] },
        { fromPort: 8080, toPort: 8080, protocol: "tcp", description: "App port",  cidrBlocks: ["0.0.0.0/0"] },
    ],
    egress: [
        { fromPort: 0, toPort: 0, protocol: "-1", cidrBlocks: ["0.0.0.0/0"] },
    ],
    tags: { Name: `${env}-app-sg` },
});

export const securityGroupId = app.id;

Common mistakes

ubx v1 does not support nested field access on dynamic element values — ingress.value.nested.field is not supported. Keep the objects in your for_each collection flat (string and number values only). If you need deeply-structured per-rule config, use separate unit blocks or a TypeScript/Python/Go component.
The label you give the dynamic block ("ingress", "egress") must match the name of the nested block attribute in the Pulumi resource schema — check the provider docs if your dynamic expansion isn’t working as expected. The label is also how you reference the current element: ingress.value, egress.value, etc.

Run it

ubx validate
ubx plan   # requires AWS credentials

What you learned

dynamic generates one nested block per element in a for_each collection
The current element is accessed as <block_name>.value inside the content sub-block
The generated code expands to explicit nested block objects — no runtime loops

Next steps

Multi-environment with extend

Override attributes per environment with extend blocks

for_each over a list

Create multiple top-level resources from a list