The most surprising thing about Pulumi’s resource graph is that it’s not just a pretty picture; it’s the system’s brain, dictating the order in which your infrastructure is created, updated, and deleted.
Let’s see it in action. Imagine you’re deploying a simple web application: a container service and a database.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Create a database instance
const db = new aws.rds.Instance("app-db", {
allocatedStorage: 20,
engine: "mysql",
engineVersion: "5.7",
instanceClass: "db.t3.micro",
skipFinalSnapshot: true,
});
// Create a security group that allows access from anywhere (for simplicity)
const webSg = new aws.ec2.SecurityGroup("web-sg", {
description: "Allow HTTP and HTTPS access",
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
],
});
// Create a container service that depends on the database
const app = new aws.ecs.Service("app-service", {
cluster: new aws.ecs.Cluster("app-cluster").arn, // Assume cluster is created or defined elsewhere
taskDefinition: new aws.ecs.TaskDefinition("app-task", {
family: "app-task-family",
cpu: "256",
memory: "512",
containerDefinitions: pulumi.output(JSON.stringify([{
name: "app-container",
image: "nginx", // Replace with your actual app image
portMappings: [{ containerPort: 80, hostPort: 80 }],
environment: [
{ name: "DATABASE_HOST", value: db.address }, // Injecting DB address
{ name: "DATABASE_PORT", value: db.port }, // Injecting DB port
],
}])).apply(JSON.parse),
requiresCompatibilities: ["FARGATE"],
networkMode: "awsvpc",
executionRoleArn: new aws.iam.Role("ecs-exec-role", {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: "ecs-tasks.amazonaws.com" }),
}).arn,
}).arn,
launchType: "FARGATE",
networkConfiguration: {
subnets: aws.ec2.getSubnets({
filters: [{ name: "vpc-id", values: [aws.ec2.getVpc({ default: true }).id] }],
}).ids,
securityGroups: [webSg.id],
},
desiredCount: 1,
// Explicitly declare dependency on the database instance
// Pulumi often infers this through `db.address` and `db.port` in task definition,
// but explicit `dependsOn` is good practice for clarity and complex cases.
dependsOn: [db],
});
export const appEndpoint = app.ingressEgress.apply(ingressEgress => ingressEgress[0].host);
export const dbAddress = db.address;
When Pulumi runs pulumi up, it doesn’t just iterate through your code line by line. It builds a directed acyclic graph (DAG) of your resources. Each node in the graph is a resource, and an edge represents a dependency. For example, the aws.ecs.Service depends on the aws.rds.Instance because the task definition references db.address and db.port. Pulumi sees this reference and knows it can’t create the ECS service until the RDS instance is ready and has an address and port.
You can visualize this graph by running pulumi graph. This command outputs a DOT language representation of the graph, which can then be rendered into an image using tools like Graphviz. The graph clearly shows which resources must be created or updated before others. Notice the arrows: they point from a resource to the resources it directly or indirectly depends on.
The mental model is this: Pulumi first analyzes all the resources you’ve declared and their explicit or implicit dependencies. It then uses this graph to determine the optimal order of operations. For creation, it performs a topological sort to ensure all dependencies are met before proceeding. For updates and deletions, it also respects these relationships to prevent cascading failures. If you tried to delete the database before the application service that uses it, Pulumi would prevent that, or at least attempt to delete the application first if it were configured to do so.
The dependsOn option is your explicit way to tell Pulumi about a dependency that might not be obvious from resource property references. For instance, if your application configuration file needs to be deployed to a server after that server is provisioned, but the server creation logic doesn’t directly reference any outputs from the server resource, you’d use dependsOn to link them.
The resource graph is also dynamic. When you update a resource, Pulumi re-evaluates the graph. It identifies which parts of the graph are affected by your change and plans the minimal set of updates required. This is key to Pulumi’s efficiency – it doesn’t just tear down and rebuild everything; it intelligently updates only what’s necessary, guided by the dependency structure.
A common point of confusion arises when a resource appears to be created before its dependency, but Pulumi handles it correctly. This often happens with implicit dependencies derived from output property references. For example, if a security group rule references an IP address that’s an output of another resource, Pulumi will correctly infer that the security group creation must wait for that IP address to be available. The graph visually represents these inferred edges, not just the explicit dependsOn clauses.
The next concept to grapple with is how Pulumi manages state and uses the resource graph to reconcile desired infrastructure with the actual state in your cloud provider.