Pulumi Inputs, Outputs & Dependencies

Medium 22 min read

Understanding Inputs

Why Inputs & Outputs Matter

The Problem: Cloud resources depend on each other - a subnet needs a VPC ID, a security group needs a VPC ID, an instance needs both. These values aren't known until resources are actually created.

The Solution: Pulumi's Input/Output type system lets you pass values between resources even before they exist, automatically tracking dependencies and ordering operations correctly.

Real Impact: You write code that looks synchronous, but Pulumi handles all the async complexity of creating resources in the right order behind the scenes.

Real-World Analogy

Think of Inputs and Outputs like a relay race:

  • Input<T> = The baton a runner is ready to receive (a value or a promise of a value)
  • Output<T> = The baton a runner will hand off after finishing their leg
  • .apply() = The handoff zone where you transform the baton
  • pulumi.all() = Waiting for multiple runners to finish before starting the next leg
  • Dependency Graph = The race schedule determining the order of legs

Input Types

Plain Values

Regular strings, numbers, and booleans that are known at program execution time. These are the simplest inputs.

Output Values

Values produced by other resources that won't be known until the resource is created in the cloud.

Promise Values

Async values from API calls or computations that resolve during the Pulumi program execution.

Input<T> Union

A type that accepts any of the above: T | Output<T> | Promise<T>, giving you maximum flexibility.

input-types.ts
import * as aws from "@pulumi/aws";

// Plain value input
const bucket = new aws.s3.Bucket("data", {
    acl: "private",  // plain string
});

// Output value as input (automatic dependency)
const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
});

const subnet = new aws.ec2.Subnet("app", {
    vpcId: vpc.id,  // Output<string> used as Input<string>
    cidrBlock: "10.0.1.0/24",
});

Working with Outputs

output-basics.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("my-bucket");

// bucket.id is Output<string> - not a plain string!
// You CANNOT do: console.log("ID: " + bucket.id)

// Instead, use .apply() to access the value
bucket.id.apply(id => {
    console.log(`Bucket ID: ${id}`);
});

// Or use pulumi.interpolate for string concatenation
const bucketUrl = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;

// Export outputs for other stacks or CLI access
export const url = bucketUrl;

Output Chaining

chaining.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("data");

// Chain .apply() calls to transform outputs
const upperName = bucket.bucket.apply(name =>
    name.toUpperCase()
);

// .apply() returns a new Output, so you can chain
const nameLength = bucket.bucket
    .apply(name => name.length)
    .apply(len => `Name has ${len} characters`);

// Use pulumi.interpolate for cleaner string building
const endpoint = pulumi.interpolate`https://${bucket.bucketRegionalDomainName}/index.html`;

Apply and All

Dependency Graph Between Resources
VPC (10.0.0.0/16) Subnet A (vpc.id) Security Group (vpc.id) Subnet B (vpc.id) EC2 Instance (subnet.id + sg.id) pulumi.all([subnet.id, sg.id])
apply-all.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const vpc = new aws.ec2.Vpc("main", { cidrBlock: "10.0.0.0/16" });
const subnet = new aws.ec2.Subnet("app", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
});

// pulumi.all() - combine multiple outputs
const info = pulumi.all([vpc.id, subnet.id]).apply(([vpcId, subnetId]) => {
    return `VPC: ${vpcId}, Subnet: ${subnetId}`;
});

// Using output properties directly as inputs
const sg = new aws.ec2.SecurityGroup("web", {
    vpcId: vpc.id,  // implicit dependency
    ingress: [{
        protocol: "tcp",
        fromPort: 80,
        toPort: 80,
        cidrBlocks: ["0.0.0.0/0"],
    }],
});

// pulumi.all with object syntax
const summary = pulumi.all({
    vpcId: vpc.id,
    subnetId: subnet.id,
    sgId: sg.id,
}).apply(vals => {
    return JSON.stringify(vals, null, 2);
});

Dependency Graph

How Dependencies Work

  • Implicit: When you pass an Output as an Input, Pulumi automatically creates a dependency
  • Explicit: Use dependsOn resource option for dependencies not captured by data flow
  • Parallel: Resources without dependencies are created in parallel for faster deployments
  • Ordering: Pulumi always creates dependencies before dependents, and deletes in reverse order

Common Pitfall

Problem: Trying to use console.log(bucket.id) prints [Output] instead of the actual value.

Solution: Output values are promises that resolve during deployment. Use .apply() to access the resolved value, or pulumi.interpolate for string interpolation.

Quick Reference

Input/Output Operations

Operation Description Example
.apply(fn) Transform an output value vpc.id.apply(id => id.toUpperCase())
pulumi.all() Combine multiple outputs pulumi.all([a.id, b.id]).apply(([a, b]) => ...)
pulumi.interpolate String interpolation with outputs pulumi.interpolate`arn:${id}`
pulumi.concat() Concatenate outputs and strings pulumi.concat("prefix-", name)
pulumi.output() Wrap a plain value as Output pulumi.output("hello")
export const Export stack output export const url = bucket.websiteEndpoint