Pulumi determines the order in which resources are created by tracking how they reference each other. If a resource depends on another, Pulumi makes sure that the first resource is provisioned before the dependent one. This works automatically when a resource references another resource’s output. For example, if a subnet uses vpc.id, Pulumi knows the VPC must be created first.

When working with AWS infrastructure, Pulumi makes sure that dependencies between cloud resources like VPCs, security groups, and IAM roles are correctly tracked. However, in cases where dependencies are not automatically detected, using dependsOn can help enforce the proper execution order.

Pulumi’s automatic tracking makes sure that the resources with direct references are created in the correct order. However, when resources do not have any explicit dependencies, Pulumi may attempt to create them in parallel. This can lead to deployment issues, such as a security group not being fully configured before an instance that relies on its rules is created. In such cases, dependsOn can be used to enforce proper ordering.

A common example is provisioning an EC2 instance with a security group. The instance configuration includes the security group ID, but Pulumi treats this as a static value rather than a dependency. Without any intervention, Pulumi may attempt to create the security group and the instance in parallel. While AWS makes sure that the security group exists before the instance is launched, network rules might not be fully applied when the instance becomes active. This could lead to temporary connectivity issues. 

Some AWS services require additional time to become available after creation. IAM roles, for example, take a few seconds to propagate across AWS regions. If an EC2 instance is created immediately after an IAM role, it may fail to assume the role because it doesn’t exist yet from AWS’s perspective. This often happens in automated deployments where instances rely on IAM roles to access other AWS services.

Different Ways to Define Dependencies in Pulumi

Now, Pulumi provides multiple ways to define and manage dependencies based on how resources interact. Some dependencies are implicit, meaning Pulumi automatically determines the order based on direct references (e.g., an EC2 instance referencing a subnet ID). Other dependencies require explicit definition using dependsOn, especially when Pulumi does not detect them correctly (e.g., ensuring an IAM role is fully available before an EC2 instance is launched).

In larger infrastructures, dependencies can also extend beyond a single Pulumi stack, requiring cross-stack references. For example, a networking stack might create a VPC and subnets, while an application stack deploys instances that rely on those networking resources. In such cases, pulumi.StackReference is used to fetch dependencies from another stack.

Automatic Dependencies

Pulumi infers dependencies when one resource directly references another. If a resource depends on the output of another, Pulumi ensures the first resource is created before the second. This happens without any manual configuration.

A common example is creating an EC2 instance inside a subnet. Since the instance references subnet.id, Pulumi automatically makes sure that the subnet is created first.

const vpc = new aws.ec2.Vpc("main-vpc", { cidrBlock: "10.0.0.0/16", }); const subnet = new aws.ec2.Subnet("main-subnet", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24", }); const instance = new aws.ec2.Instance("web-server", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: subnet.id, // Pulumi detects dependency here });

Pulumi will always create the VPC first, then the subnet, and finally the EC2 instance because the resources reference each other naturally. No extra configuration is required.

However, automatic dependency tracking only works when a resource directly references another. If an ID is passed as a string or an external value, Pulumi does not track the dependency correctly, and resources may be created in parallel.

Manual Dependencies with dependsOn

When resources do not reference each other directly, Pulumi may create them in the wrong order. This happens when an EC2 instance needs a security group, but the security group ID is simply passed as a value rather than referenced directly.

If the security group is not fully provisioned before the instance is created, the instance may launch without the correct network rules, causing connectivity issues. To prevent this, dependsOn is used to enforce the dependency explicitly.

const securityGroup = new aws.ec2.SecurityGroup("instance-sg", { description: "Allow SSH", ingress: [{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }], }); const instance = new aws.ec2.Instance("web-server", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", vpcSecurityGroupIds: [securityGroup.id], dependsOn: [securityGroup], // Ensures security group is created first });

Here, depends on, which makes sure the security group exists before the instance is provisioned. Without it, Pulumi may attempt to create them at the same time, leading to unpredictable behavior.

Another scenario where dependsOn is necessary is when an IAM role needs to be attached to an EC2 instance. IAM roles take time to propagate within AWS. If an instance tries to assume a role immediately after it’s created, it may fail because the role is not yet fully available.

const role = new aws.iam.Role("ec2-role", { assumeRolePolicy: JSON.stringify({ Version: "2012-10-17", Statement: [{ Action: "sts:AssumeRole", Effect: "Allow", Principal: { Service: "ec2.amazonaws.com" }], }), }); const instanceProfile = new aws.iam.InstanceProfile("instance-profile", { role: role.name, dependsOn: [role], // Makes sure that the IAM role is created before the instance profile });

Without dependsOn, Pulumi may try to create the instance profile before the role has fully propagated, causing an error.

Cross-Stack Dependencies

In large infrastructure deployments, resources are often managed across multiple Pulumi stacks. A networking stack might create a VPC and subnets, while an application stack deploys EC2 instances, databases, and Kubernetes clusters that rely on those networking resources.

Pulumi does not automatically track dependencies across stacks, so stack references are used to fetch outputs from one stack into another.

Suppose the networking stack (networking) exports a VPC ID and a subnet ID:

export const vpcId = vpc.id; export const subnetId = subnet.id;

The application stack can retrieve these values using pulumi.StackReference:

const networkingStack = new pulumi.StackReference("org/networking/dev"); const vpcId = networkingStack.getOutput("vpcId"); const subnetId = networkingStack.getOutput("subnetId"); const instance = new aws.ec2.Instance("app-server", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: subnetId, // Pulumi makes sure that the subnet exists first });

This ensures that the VPC and subnet from the networking stack are created before any dependent resources are added to the application stack. Without this, the application stack might try to deploy resources into a non-existent network.

When to Use Each Approach

Pulumi’s automatic dependency tracking works well when resources naturally reference each other, but in cases where they don’t, dependencies must be manually defined. If a resource needs to be fully ready before another can use it, dependsOn is required. For large environments, stack references ensure that resources across stacks are created in the correct order.

What Are The Techniques to Control Dependencies?

Pulumi provides multiple ways to control dependencies and ensure resources are created in the right order. While automatic tracking works in most cases, there are situations where dependencies need to be explicitly defined to prevent failures. The right approach depends on how resources interact within a stack or across multiple stacks.

Using Outputs to Define Dependencies

When a resource depends on another, its configuration should reference the first resource’s output rather than hardcode values. This makes sure that Pulumi establishes a natural dependency.

For example, instead of manually specifying a subnet ID for an EC2 instance, reference it directly from the subnet resource:

const vpc = new aws.ec2.Vpc("main-vpc", { cidrBlock: "10.0.0.0/16" }); const subnet = new aws.ec2.Subnet("main-subnet", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24", }); const instance = new aws.ec2.Instance("web-server", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: subnet.id, // Ensures subnet is created first });

Since this example uses AWS resources, ensure that the Pulumi AWS provider is set up correctly. If you're using Pulumi for the first time with AWS, configure the provider by running pulumi config set aws:region us-east-1. This command tells Pulumi which AWS region to deploy resources in and ensures that all resources use the Pulumi AWS provider for seamless provisioning.

By passing subnet.id instead of a hardcoded value, Pulumi automatically makes sure that the subnet is created before the EC2 instance.

Using dependsOn for Explicit Dependencies

When resources do not directly reference each other but still need to be created in a specific order, dependsOn forces Pulumi to respect that dependency.

For example, an IAM role needs to be fully available before it is assigned to an EC2 instance:

const role = new aws.iam.Role("ec2-role", { assumeRolePolicy: JSON.stringify({ Version: "2012-10-17", Statement: [{ Action: "sts:AssumeRole", Effect: "Allow", Principal: { Service: "ec2.amazonaws.com" } }], }), }); const instanceProfile = new aws.iam.InstanceProfile("instance-profile", { role: role.name, dependsOn: [role], // Ensures IAM role is created before the instance profile });

This prevents failures caused by AWS IAM propagation delays.

Using pulumi.all() for Multiple Dependencies

Some resources depend on multiple others, requiring a combined dependency check. pulumi.all() gathers outputs from multiple resources and ensures they are all available before proceeding.

For example, if an application deployment needs both a VPC and an S3 bucket:

const vpc = new aws.ec2.Vpc("app-vpc", { cidrBlock: "10.0.0.0/16" }); const bucket = new aws.s3.Bucket("app-bucket"); pulumi.all([vpc.id, bucket.id]).apply(([vpcId, bucketId]) => { console.log(`VPC ID: ${vpcId}, S3 Bucket ID: ${bucketId}`); });

This ensures the VPC and S3 bucket exist before any dependent operations run.

Using Stack References for Cross-Stack Dependencies

When working with multiple stacks, resources from one stack need to be referenced in another. Pulumi does not automatically track cross-stack dependencies, so pulumi.StackReference is used to fetch outputs from a different stack.

For example, if a networking stack defines a VPC and a compute stack needs to use it:

const networkingStack = new pulumi.StackReference("org/networking/dev"); const vpcId = networkingStack.getOutput("vpcId"); const instance = new aws.ec2.Instance("compute-server", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: networkingStack.getOutput("subnetId"), });

This ensures that the VPC and subnet are created in the networking stack before the compute stack provisions any instances.

Controlling Dependencies for Stability

Using outputs makes sure that the dependencies are naturally tracked, while dependsOn handles cases where Pulumi does not infer dependencies correctly. pulumi.all() is useful for handling multiple dependencies together, and pulumi.StackReference ensures that resources across stacks are created in the right order. Choosing the right technique depends on the complexity of the infrastructure and how resources interact.

Setting Up Dependencies in Pulumi

Now that we know how Pulumi handles dependencies, it's time to apply these concepts in a real-world scenario. The goal is to set up a fully functional infrastructure that includes a VPC, a subnet, an EC2 instance with an attached IAM role, a security group for network access, and an independent S3 bucket. Pulumi will automatically track dependencies where possible, but explicit dependencies will be added where necessary to prevent race conditions and ensure stability.

Before getting started, you need to install Pulumi on your system. You can do this by running:

curl -fsSL https://get.pulumi.com | sh

For more details, refer to the official Pulumi install guide.

Once Pulumi is installed, authenticate by running pulumi login.

Now, the process begins with defining the network layer. A VPC is created first, followed by a subnet inside it. Since the subnet references the VPC ID, Pulumi automatically ensures the VPC exists before proceeding with the subnet.

import * as aws from "@pulumi/aws"; import * as pulumi from "@pulumi/pulumi"; const vpc = new aws.ec2.Vpc("firefly-vpc", { cidrBlock: "10.0.0.0/16", tags: { Name: "firefly-vpc" }, }); const subnet = new aws.ec2.Subnet("firefly-subnet", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24", availabilityZone: "us-east-1a", tags: { Name: "firefly-subnet" }, });

Now, run pulumi up command. Once applied, Pulumi first provisions the VPC and then proceeds with the subnet.

With the network layer in place, the next step is defining the security group that will control inbound access. The security group allows SSH connections. Pulumi does not automatically infer dependencies between security groups and instances, so dependsOn is used to ensure the security group is fully provisioned before the instance is created.

const securityGroup = new aws.ec2.SecurityGroup("firefly-sg", { vpcId: vpc.id, description: "Allow SSH", ingress: [{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }], tags: { Name: "firefly-sg" }, }); const ec2Instance = new aws.ec2.Instance("firefly-ec2", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: subnet.id, vpcSecurityGroupIds: [securityGroup.id], tags: { Name: "firefly-ec2" }, dependsOn: [securityGroup], });

Pulumi provisions the security group before launching the EC2 instance.

With the compute instance in place, an IAM role needs to be attached to grant necessary permissions. AWS IAM roles require time to propagate across regions, and if an instance profile is created immediately after defining the role, AWS may not recognize it yet. To avoid failures, dependsOn, which is used to ensure that the IAM role is fully available before the instance profile is assigned.

const role = new aws.iam.Role("firefly-iam-role", { assumeRolePolicy: JSON.stringify({ Version: "2012-10-17", Statement: [{ Action: "sts:AssumeRole", Effect: "Allow", Principal: { Service: "ec2.amazonaws.com" } }], }), tags: { Name: "firefly-iam-role" }, }); const instanceProfile = new aws.iam.InstanceProfile("firefly-instance-profile", { role: role.name, dependsOn: [role], }); const updatedInstance = new aws.ec2.Instance("firefly-ec2-updated", { ami: "ami-04b4f1a9cf54c11d0", instanceType: "t2.micro", subnetId: subnet.id, vpcSecurityGroupIds: [securityGroup.id], iamInstanceProfile: instanceProfile.name, tags: { Name: "firefly-ec2" }, dependsOn: [instanceProfile], });

An independent S3 bucket is also added. Since it does not rely on the network or compute layers, Pulumi creates it in parallel without waiting for other resources.

const s3Bucket = new aws.s3.Bucket("firefly-bucket", { acl: "private", tags: { Name: "firefly-bucket" }, });

For situations where multiple dependencies need to be resolved before proceeding, pulumi.all() is used to ensure that all required resources are available before performing further actions.

pulumi.all([vpc.id, subnet.id, s3Bucket.bucket]).apply(([vpcId, subnetId, bucketName]) ⇒ { console.log(`VPC: ${vpcId}, Subnet: ${subnetId}, S3 Bucket: ${bucketName}`); });

At this point, all resources have been successfully provisioned. When tearing down infrastructure, Pulumi deletes resources in reverse dependency order to avoid breaking dependencies. Running pulumi destroy first removes the EC2 instance, then the security group, followed by the subnet and VPC.

Pulumi ensures that dependencies are respected at every stage. Resources are created in the correct order, and deletions happen in a structured way, preventing any broken infrastructure states.

Best Practices for Managing Resource Dependencies in Pulumi

Now that we have seen how to define and manage resource dependencies in Pulumi, it's important to follow some of the best practices to ensure efficient, scalable, and error-free deployments.

Use Outputs Instead of Hardcoded Values

Pulumi automatically tracks dependencies when a resource references another resource’s output. Instead of manually specifying values like subnet IDs or security group IDs, always pass them dynamically. This prevents inconsistencies and ensures resources are created in the correct order.

Keep dependsOn Minimal

While dependsOn is useful, it should be kept minimal. Pulumi infers most dependencies when one resource references another, and adding unnecessary dependsOn constraints can slow down deployments by forcing sequential execution where parallelism is possible. Use dependsOn only when Pulumi does not automatically detect a dependency, such as IAM role propagation delays or implicit relationships that do not involve direct references.

Avoid Circular Dependencies

If two resources depend on each other, Pulumi cannot determine which one to create first, leading to deployment failures. This often happens when networking components reference each other, such as instances needing an IAM role while the IAM policy refers to the instance. Breaking these dependencies by restructuring the configuration or introducing intermediary resources can resolve such issues.

Scale Dependencies in Large Projects

As infrastructure scales, managing dependencies across large stacks becomes essential. Using Pulumi’s Stack References allows separate infrastructure components, such as networking and computing, to be managed in different stacks while still ensuring dependencies between them. This makes it easier to maintain, update, and scale deployments without affecting unrelated components.

Firefly: Codifying Unmanaged Resources in Pulumi

Now, infrastructure dependencies are not always managed through code. Some resources, like IAM roles, are often created in the AWS console for some urgent requirements. Others might be provisioned using shell scripts, AWS CLI, or Terraform, resulting in a mix of codified and unmanaged resources. This inconsistency makes tracking dependencies a difficult task. When infrastructure is only partially codified, manual modifications can introduce drift, causing failures within deployments. Without a proper codification, there is no way to ensure consistency across all environments.

Firefly provides visibility into this problem by classifying resources based on their state. It differentiates between codified resources that are already managed through IaC, unmanaged resources that exist outside of any IaC tool, and drifted resources that have changed without being updated in code. The core issue is that unmanaged resources are still dependencies within the infrastructure, but they are not represented in Pulumi, making updates and troubleshooting much harder.

Firefly solves this by offering an automatic way to generate Pulumi code for unmanaged resources. Instead of writing Pulumi configurations, Firefly scans the environment, detects unmanaged resources, and provides a ready-to-use Pulumi script. This makes it easy to bring those resources under IaC management without rewriting everything from scratch.

Once Firefly identifies these unmanaged resources, it generates Pulumi configurations in TypeScript, Python, Go, or C#, ensuring compatibility with existing workflows. The generated code can be directly applied, instantly bringing the resource under IaC control.

The above image shows the generated TypeScript code for defining an IAM policy, while the below image displays the equivalent Python configuration.

This eliminates all the inconsistencies within your infrastructure, making deployments more reliable and repeatable. By codifying unmanaged resources, teams can enforce security policies, prevent drift, and make sure that all dependencies are properly tracked within Pulumi only.

To read more about Firefly, you can check out Firefly’s documentation or log in to Firefly and explore how it simplifies your workflow by automating codification.

Frequently Asked Questions

What is the dependency between resources in Pulumi?

Pulumi automatically tracks dependencies when a resource references another’s output, ensuring resources are created in the correct order.

When should I use dependsOn?

Use dependsOn when resources do not have direct references but must be created in a specific order, such as IAM roles that take time to propagate.

Can I reference resources from another Pulumi stack?

Yes, you can use pulumi.StackReference to fetch outputs from another stack, ensuring cross-stack dependencies are correctly handled.

How can I debug dependency issues in Pulumi?

Check Pulumi's preview plan, inspect stack outputs, and review logs to identify missing dependencies or unintended parallel executions.