Inputs, Outputs and the Dependency Graph
Inputs, Outputs and the Dependency Graph
A Pulumi program looks imperative: Python statements, run top to bottom. The infrastructure it describes is declarative: Pulumi decides what to create, in what order, and what to leave alone. The bridge between the two is a pair of types. Inputs are what you give a resource. Outputs are values the cloud reports back later. Every confusing moment for new Pulumi users (“why does my bucket name print as an object?”) comes from treating an output as if it were already a value.
pulumi and the provider SDKs were not available on the authoring box and installs were not allowed, so these Python snippets were not executed. They follow the current Pulumi documentation and SDK method names.
Learning goals
- Distinguish inputs, resource outputs and stack outputs
- Explain how passing an output into an input creates a dependency edge
- Transform outputs with
apply,Output.all,Output.format,Output.concatand the JSON helpers, and reach nested values by lifting - Recognise the “output used as a plain value” bug, and why resources must not be created inside
apply
Inputs
An input is an argument to a resource, usually one parameter of the underlying cloud API call. Every input of type Input[T] also accepts a plain T:
import pulumi_random as random
my_id = random.RandomId("mine", byte_length=8) # 8 is a plain value used as an inputPython offers two spellings for object-shaped inputs: an ...Args class or a dictionary typed as ...ArgsDict. Both carry the same fields, and the dictionary form is shorter:
import pulumi_aws as aws
repo = aws.ecr.Repository(
"repo",
image_tag_mutability="MUTABLE",
image_scanning_configuration={"scan_on_push": True},
)Outputs
An output is a property the provider reports after the resource exists, such as an ID, an ARN, an endpoint or a generated password. Outputs behave like futures. During a first pulumi up the value is unknown until the cloud answers. During pulumi preview it may never be known.
Pass an output straight into another resource’s input and Pulumi records a dependency:
import pulumi_aws as aws
import pulumi_random as random
password = random.RandomPassword("password", length=16, special=True)
db = aws.rds.Instance(
"db",
instance_class="db.t3.micro",
allocated_storage=64,
engine="mysql",
username="someone",
password=password.result, # output -> input: an edge in the graph
)That edge tells Pulumi to create the password before the database, and on destroy to delete the database before the password. You wrote no ordering code. A program that wires outputs to inputs gets correct ordering for free, and depends_on is only for dependencies that no value carries.
Three similar names refer to different things:
| Term | What it is |
|---|---|
| Input | An argument to a resource; accepts plain values or outputs |
| Resource output | A property of a resource, known once the provider reports it |
| Stack output | A value you pulumi.export at the end of a run for the CLI or a StackReference |
Getting at the value
Because an output is not yet a value, you can’t use ordinary language operations on it. print(bucket.id) or "https://" + lb.dns_name doesn’t do what you expect. The SDK supplies the tools instead.
apply: transform one output
url = lb.dns_name.apply(lambda name: f"https://{name}")The callback runs when the value is known and receives the plain value. Its return value becomes a new Output, which you can pass on as an input or export. Secret-ness and dependencies flow through.
Output.all: combine several
import pulumi
conn = pulumi.Output.all(server.name, database.name).apply(
lambda args: f"Server=tcp:{args[0]}.database.windows.net;initial catalog={args[1]};"
)
# keyword form yields a dict
conn = pulumi.Output.all(server=server.name, db=database.name).apply(
lambda a: f"Server=tcp:{a['server']}.database.windows.net;initial catalog={a['db']};"
)Helpers: skip the lambda
Most apply calls build a string or some JSON. The helpers do that directly and are easier to read:
import json
import pulumi
url = pulumi.Output.format("https://{0}/api", lb.dns_name)
arn_path = pulumi.Output.concat(bucket.arn, "/*")
policy = pulumi.Output.json_dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": [arn_path], # outputs allowed inside the structure
}],
})
parsed = pulumi.Output.json_loads(some_json_output)json_dumps accepts a structure that has outputs nested inside it. A json.dumps inside an apply would make you resolve each one by hand first.
Lifting: reach into nested outputs
Properties and indexes can be read straight off an output, with no apply. Pulumi “lifts” the access:
record_name = cert.domain_validation_options[0].resource_record_nameWithout lifting this would be cert.domain_validation_options.apply(lambda o: o[0].resource_record_name).
Two rules that prevent most bugs
1. Never treat an output as a plain value. Printing it, concatenating it with +, using it in an if, or formatting it with an f-string outside apply gives you the wrapper object, not the value. Python’s Output warns when converted to a string, and the result ends up in resource names and tags looking like garbage. Use apply, format, concat or json_dumps.
2. Don’t create resources inside apply or all. Pulumi’s documentation says to avoid it wherever possible. Resources created in a callback don’t appear in pulumi preview unless the value is already known, so the preview stops matching what up will do. Reviewers then approve a plan that leaves things out. If the number of resources depends on a value, read that value from configuration (known at preview), not from another resource’s output.
Resource identity
Wiring resources means passing some identifier, and the inputs differ in which one they want. A resource has four identities: its logical name (the first constructor argument), its physical name at the provider, its physical ID (resource.id), and its Pulumi URN. Many inputs want an ARN, an endpoint or a name instead of the ID. A type mismatch at preview usually means you passed the wrong identity. Check the input’s documentation.
Diagnosis
| Symptom | Cause | Fix |
|---|---|---|
A name or tag contains text about calling __str__ on an Output |
Output used in an f-string or str() |
Output.format/concat, or build it in apply |
Preview shows fewer resources than up creates |
Resources created inside apply |
Drive counts from config. Create resources at top level |
| Resources created in the wrong order | The dependency is not carried by any value | Pass the output as an input, or depends_on as a last resort |
Type error passing bucket where a string is expected |
Passed the resource, not a property | bucket.id, bucket.arn or bucket.bucket, per the input’s docs |
| JSON policy has a placeholder where an ARN belongs | json.dumps on a structure containing outputs |
Output.json_dumps |
Practice
- Build an output URL
https://<dns>/healthfrom a load balancer’sdns_namethree ways:apply,Output.formatandOutput.concat. Which reads best? - A colleague loops
for i in range(int(count_output)). Explain why this fails and rewrite it to use config. - Write an S3 bucket policy that references two buckets’ ARNs, using only
Output.json_dumps.
Recall card
- Inputs accept plain values or outputs. Outputs are future values reported by the provider.
- Output to input means a dependency edge. Ordering for create and destroy is automatic.
applyhandles one output,Output.allseveral, andformat/concat/json_dumps/json_loadsare the common shortcuts. Lifting reads nested fields.- Never treat an output as plain, and never create resources inside
apply.
Sources
- Pulumi, Inputs and outputs
- Pulumi, Accessing single output values with apply
- Pulumi, Accessing multiple output values with all