Loops and Iteration
A loop repeats an action once for each item in a collection. This is the
construct behind count and for_each in HCL: a way to create a resource
multiple times, once per item in a list or map, without writing the resource
block out by hand for each one. A general-purpose language gives you an
ordinary loop, and it applies to resources exactly the way it applies to any
other value.
Where you have seen this before
Pulumi YAML has no native looping construct, so a fixed-size resource
collection is usually spelled out explicitly. Terraform HCL’s for_each is
the direct analogue of the loops in this guide:
variable "availability_zones" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "app" {
for_each = toset(var.availability_zones)
availability_zone = each.value
cidr_block = "10.0.${index(var.availability_zones, each.value)}.0/24"
}
The syntax
Each language has an idiomatic way to iterate a list: for...of in
TypeScript, a for loop in Python, for ... range in Go, foreach in C#,
and an enhanced for loop in Java.
const zones = ["us-east-1a", "us-east-1b", "us-east-1c"];
for (const zone of zones) {
console.log(zone);
}
TypeScript also has .map(), which builds a new array by applying a function
to every element, and reads well when the loop’s whole purpose is to produce
a value per item, which is exactly the shape of most resource loops:
const upper = zones.map((zone) => zone.toUpperCase());
zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
for zone in zones:
print(zone)
A list comprehension is Python’s equivalent of .map(), building a new list
from an existing one in a single expression:
upper = [zone.upper() for zone in zones]
zones := []string{"us-east-1a", "us-east-1b", "us-east-1c"}
for _, zone := range zones {
fmt.Println(zone)
}
Go has no built-in map-style function; a for ... range loop that appends
to a new slice is the idiomatic way to transform a list.
var zones = new[] { "us-east-1a", "us-east-1b", "us-east-1c" };
foreach (var zone in zones)
{
Console.WriteLine(zone);
}
LINQ’s .Select() is C#’s equivalent of .map(), useful when the loop’s job
is to produce a new sequence rather than perform a side effect:
var upper = zones.Select(zone => zone.ToUpper());
var zones = List.of("us-east-1a", "us-east-1b", "us-east-1c");
for (var zone : zones) {
System.out.println(zone);
}
The Stream API’s .map() is Java’s equivalent, useful when you’re
transforming a list into another list rather than iterating for a side
effect:
var upper = zones.stream().map(String::toUpperCase).toList();
Pulumi YAML has no looping construct of any kind, functional or otherwise. Every resource in the file has to be written out explicitly.
variable "availability_zones" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "app" {
for_each = toset(var.availability_zones)
availability_zone = each.value
}
In a Pulumi program
Creating one subnet per availability zone is the same loop, with a resource declaration as the body. The index or the zone name becomes part of the resource’s logical name, so each iteration produces a distinct resource instead of overwriting the same one:
import * as aws from "@pulumi/aws";
const zones = ["us-east-1a", "us-east-1b", "us-east-1c"];
const subnets = zones.map(
(zone, index) =>
new aws.ec2.Subnet(`app-subnet-${zone}`, {
availabilityZone: zone,
cidrBlock: `10.0.${index}.0/24`,
}),
);
import pulumi_aws as aws
zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
subnets = [
aws.ec2.Subnet(
f"app-subnet-{zone}",
availability_zone=zone,
cidr_block=f"10.0.{index}.0/24",
)
for index, zone in enumerate(zones)
]
package main
import (
"fmt"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
zones := []string{"us-east-1a", "us-east-1b", "us-east-1c"}
for index, zone := range zones {
_, err := ec2.NewSubnet(ctx, fmt.Sprintf("app-subnet-%s", zone), &ec2.SubnetArgs{
AvailabilityZone: pulumi.String(zone),
CidrBlock: pulumi.String(fmt.Sprintf("10.0.%d.0/24", index)),
})
if err != nil {
return err
}
}
return nil
})
}
using Pulumi;
using Pulumi.Aws.Ec2;
return await Deployment.RunAsync(() =>
{
var zones = new[] { "us-east-1a", "us-east-1b", "us-east-1c" };
var subnets = zones.Select((zone, index) => new Subnet($"app-subnet-{zone}", new SubnetArgs
{
AvailabilityZone = zone,
CidrBlock = $"10.0.{index}.0/24",
})).ToList();
});
import com.pulumi.Pulumi;
import com.pulumi.aws.ec2.Subnet;
import com.pulumi.aws.ec2.SubnetArgs;
public class App {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var zones = List.of("us-east-1a", "us-east-1b", "us-east-1c");
for (var i = 0; i < zones.size(); i++) {
var zone = zones.get(i);
var subnet = new Subnet("app-subnet-" + zone, SubnetArgs.builder()
.availabilityZone(zone)
.cidrBlock("10.0." + i + ".0/24")
.build());
}
});
}
}
Pulumi YAML can’t create a resource per item in a list. Each subnet has to be declared as its own resource, so a fixed, small set of zones is written out by hand:
resources:
appSubnetA:
type: aws:ec2:Subnet
properties:
availabilityZone: us-east-1a
cidrBlock: 10.0.0.0/24
appSubnetB:
type: aws:ec2:Subnet
properties:
availabilityZone: us-east-1b
cidrBlock: 10.0.1.0/24
variable "availability_zones" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "app" {
for_each = toset(var.availability_zones)
availability_zone = each.value
cidr_block = "10.0.${index(var.availability_zones, each.value)}.0/24"
}
What to watch out for
Every resource needs a unique logical name, the first argument to its constructor, and that name is part of the resource’s identity in Pulumi’s state. When you loop, build the name from something stable, such as the zone or a fixed index, rather than an order that could shift between updates; changing the name replaces the resource. See resource names for how logical names, URNs, and physical IDs relate.
Don’t iterate an output directly the way you’d iterate a plain list. A list of
resource outputs isn’t resolved until each resource exists, so looping over it
with a plain for won’t see real values yet. Call .apply() on a single
output, or pulumi.all([...]) to wait on several at once, and iterate inside
the callback instead, where the values are resolved:
pulumi.all(subnets.map((subnet) => subnet.id)).apply((ids) =>
ids.forEach((id) => console.log(id)));
See working with outputs and combining outputs for the full picture in every language.
Next steps
Continue to functions to see how to package a repeated set of resources behind a single call.