JSON in Pulumi: Stack State, Outputs, Config, and YAML/Markdown Programs
Last updated:
Pulumi is a programming-language infrastructure tool — you write TypeScript, Python, Go, C#, or Java, and the engine reconciles your code with the cloud — but JSON sits at almost every boundary the engine exposes. Stack state is persisted as a JSON checkpoint in S3, Azure Blob, GCS, or Pulumi Cloud. Stack outputs are read by CI through pulumi stack output --json. IAM policies, AWS resource fields, and Kubernetes manifests are JSON documents you embed in your program. The newer Pulumi YAML programming language accepts both YAML and JSON form, so any JSON template can run as a Pulumi program directly. This guide walks every surface where JSON shows up, with code examples in TypeScript and Python, comparison tables against CloudFormation and Terraform, and the CI patterns teams use in production.
Inspecting a pulumi stack export dump and the JSON refuses to parse? Paste it into Jsonic's JSON Validator — it points to the exact line where the checkpoint truncated or a secret cipher block went wrong.
Where Pulumi uses JSON: state, stack outputs, config, IAM policies
Even though you write Pulumi in a real programming language, JSON shows up at four specific boundaries — and understanding which boundary you are at makes the rest of the tooling obvious.
1. Stack state. The Pulumi engine keeps a JSON checkpoint of every resource it manages. The format is the same regardless of backend (Pulumi Cloud, S3, Azure Blob, GCS, local file) and is the source of truth for diff calculations, imports, and refresh operations.
2. Stack outputs. Anything you export from your program becomes a queryable output. The CLI emits these as JSON for CI consumption — pulumi stack output --json — so downstream pipelines can read identifiers that did not exist until pulumi up ran.
3. Configuration. Stack config lives in Pulumi.<stack>.yaml (a YAML file, but readable as JSON since the syntaxes overlap on the simple subset Pulumi uses). The pulumi config get --json command dumps the full config tree as JSON for inspection or scripting.
4. Embedded JSON payloads. AWS IAM policies, Kubernetes manifests, CloudWatch log group filter patterns, EventBridge rule bodies — these are JSON documents that the cloud provider expects as a string. Your Pulumi program builds them as native objects and serializes at the boundary.
Each surface has its own conventions and gotchas. The rest of this guide walks them one by one, with code you can drop into a real project.
pulumi stack output --json for CI consumption
The standard contract for handing values from Pulumi to a downstream pipeline step is pulumi stack output --json. After pulumi up finishes, this command emits a single JSON object — every export becomes a top-level key. Without the --json flag the CLI prints a Go-style map representation that is fine for humans but a pain to parse with jq or scripts.
# After pulumi up succeeds
$ pulumi stack output --json
{
"apiUrl": "https://api.dev.example.com",
"bucketName": "myapp-uploads-dev-7a2c9f",
"clusterEndpoint": "https://7A2C.gr7.us-east-1.eks.amazonaws.com",
"databaseUrl": "[secret]",
"vpcId": "vpc-0a1b2c3d4e5f6g7h8"
}
# Extract a single field with jq
$ pulumi stack output --json | jq -r .apiUrl
https://api.dev.example.com
# Include encrypted secrets (handle with care)
$ pulumi stack output --json --show-secrets > outputs.jsonIn a GitHub Actions job, capture the full object into a step output and reference individual fields downstream with the built-in fromJson() helper:
- name: Read Pulumi outputs
id: pulumi
run: |
cd infra
echo "json=$(pulumi stack output --json)" >> $GITHUB_OUTPUT
- name: Deploy app to ECS
env:
API_URL: ${{ fromJson(steps.pulumi.outputs.json).apiUrl }}
BUCKET: ${{ fromJson(steps.pulumi.outputs.json).bucketName }}
run: ./deploy.shFor multi-value extraction in shell, prefer one jq call producing shell-eval output over one call per field — fewer process spawns, and a single source of truth for the JSON snapshot you parsed. Treat the JSON as sensitive: it can leak account IDs, region names, and infrastructure topology even when secrets are masked.
Reading stack state JSON for backup and audit
The Pulumi state file is a JSON checkpoint of every resource the engine manages — a snapshot of the resource graph, every output (with secrets encrypted in place), and metadata about the run that produced it. Pull it down with pulumi stack export and inspect it like any JSON document.
# Export current state to a local JSON file
$ pulumi stack export --file state.json
# Pretty-print the top-level structure
$ jq '.deployment.resources | length' state.json
47
# List all resource types and URNs
$ jq -r '.deployment.resources[] | "\(.type)\t\(.urn)"' state.json
pulumi:pulumi:Stack urn:pulumi:dev::myapp::pulumi:pulumi:Stack::myapp-dev
aws:s3/bucket:Bucket urn:pulumi:dev::myapp::aws:s3/bucket:Bucket::uploads
aws:iam/role:Role urn:pulumi:dev::myapp::aws:iam/role:Role::lambda-exec
...
# Restore state from a backup
$ pulumi stack import --file state.jsonThe checkpoint format has been stable across minor Pulumi engine versions for years — automation that reads it is safe for production. The fields you will use most often live under deployment.resources[]:
type— the resource type identifier (e.g.,aws:s3/bucket:Bucket)urn— the globally unique Pulumi resource nameoutputs— live values returned by the cloud provider (ARNs, IDs, endpoints)inputs— the desired-state values your program suppliedparent— URN of the parent resource (for component hierarchy)dependencies— URNs this resource depends on (for graph traversal)
Backup workflow: a nightly CI job runs pulumi stack export for every stack and writes the result to an S3 bucket with object versioning and a 90-day lifecycle policy. If a stack ever becomes corrupted (a botched manual edit, a bad import, an engine bug), pulumi stack import rolls it back.
pulumi.json and Pulumi YAML/JSON program format
Every Pulumi project has a Pulumi.yaml at the project root that declares the project name, runtime, description, and plugin requirements. The file is YAML by convention but a JSON equivalent (Pulumi.json, or YAML body written in JSON syntax) parses identically — JSON is a strict subset of YAML 1.2.
# Pulumi.yaml — standard YAML form
name: myapp
runtime: nodejs
description: Production API and supporting infrastructure
plugins:
providers:
- name: aws
version: 6.85.0Pulumi YAML the language (released as a first-class runtime in 2022) is a separate thing — a declarative schema where the whole infrastructure program lives inside Pulumi.yaml itself. Set runtime: yaml and the engine parses the resource graph from the same file. Because JSON is valid YAML, every Pulumi YAML program can be expressed as JSON unchanged.
{
"name": "myapp",
"runtime": "yaml",
"resources": {
"bucket": {
"type": "aws:s3:Bucket",
"properties": {
"bucket": "myapp-uploads-prod",
"versioning": { "enabled": true },
"serverSideEncryptionConfiguration": {
"rule": {
"applyServerSideEncryptionByDefault": {
"sseAlgorithm": "AES256"
}
}
}
}
}
},
"outputs": {
"bucketName": "${bucket.id}"
}
}The JSON form is most useful when a generator emits it — a Terraform-to-Pulumi converter, a CDK8s template, or an internal stamping tool. For hand-written stacks, YAML is friendlier (no quoting of every key, comments allowed). The two forms are interchangeable on disk; the engine treats them identically.
Caveat: Pulumi YAML is a declarative subset. You get resources, variables, outputs, and a handful of control-flow primitives — but no loops, conditionals, or user-defined functions. For anything beyond a flat resource list, use TypeScript, Python, or Go.
Embedding IAM policy JSON in Pulumi programs (TS, Python, Go)
AWS IAM policies, S3 bucket policies, KMS key policies, and SQS queue policies are JSON documents — the AWS API accepts them as strings. Pulumi accepts native objects and serializes for you, which means the policy lives in your code as a typed value you can compose, validate, and lint.
// TypeScript — inline policy as a native object
import * as aws from '@pulumi/aws'
const bucket = new aws.s3.Bucket('uploads', { bucket: 'myapp-uploads' })
const role = new aws.iam.Role('lambda-exec', {
assumeRolePolicy: JSON.stringify({
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Principal: { Service: 'lambda.amazonaws.com' },
Action: 'sts:AssumeRole',
}],
}),
})
new aws.iam.RolePolicy('lambda-exec-s3', {
role: role.id,
policy: bucket.arn.apply(arn => JSON.stringify({
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['s3:GetObject', 's3:PutObject'],
Resource: `${arn}/*`,
}],
})),
})The bucket.arn.apply() pattern is important: arn is a pulumi.Output<string>, not a plain string. Inline string interpolation (`${bucket.arn}/*`) inside a top-level JSON.stringify would serialize the Output wrapper itself, not the resolved value. The apply callback receives the resolved string and produces a new Output containing the serialized policy.
# Python — same pattern with pulumi.Output.all and json.dumps
import json
import pulumi
import pulumi_aws as aws
bucket = aws.s3.Bucket('uploads', bucket='myapp-uploads')
role = aws.iam.Role('lambda-exec',
assume_role_policy=json.dumps({
'Version': '2012-10-17',
'Statement': [{
'Effect': 'Allow',
'Principal': {'Service': 'lambda.amazonaws.com'},
'Action': 'sts:AssumeRole',
}],
}),
)
aws.iam.RolePolicy('lambda-exec-s3',
role=role.id,
policy=bucket.arn.apply(lambda arn: json.dumps({
'Version': '2012-10-17',
'Statement': [{
'Effect': 'Allow',
'Action': ['s3:GetObject', 's3:PutObject'],
'Resource': f'{arn}/*',
}],
})),
)For policies you reuse across many stacks, build them with aws.iam.getPolicyDocument — a data source that takes structured input, validates the result against the IAM grammar at preview time, and produces the JSON string. This catches typos like Acction or a malformed ARN before the request ever leaves your machine. See our AWS IAM policies as JSON guide for the full policy grammar and common patterns.
Cross-stack references with StackReference and JSON outputs
Real systems split infrastructure across multiple stacks — a network stack owns the VPC, a data stack owns the database, application stacks consume both. Pulumi's first-class mechanism for this is StackReference, which reads exports from another stack and surfaces them as typed Output values that participate in the dependency graph.
# Python — reference the network stack from an application stack
import pulumi
import pulumi_aws as aws
network = pulumi.StackReference('acme/network/prod')
vpc_id = network.get_output('vpcId')
private_subnets = network.get_output('privateSubnetIds')
cluster = aws.ecs.Cluster('app-cluster', name='app')
service = aws.ecs.Service('app',
cluster=cluster.arn,
network_configuration={
'subnets': private_subnets,
'assign_public_ip': False,
},
task_definition=task_def.arn,
)
pulumi.export('serviceArn', service.id)StackReference reads the upstream checkpoint via whatever backend you configured, decrypts secret outputs with your key material, and produces real Output values — change the upstream and the dependency engine schedules updates downstream. That preserves the full graph in a way that piping JSON files between stages cannot.
The escape hatch is the file-based approach: run pulumi stack output --json --stack acme/network/prod > net.json in CI, parse the file, pass values in as config. This works between Pulumi and non-Pulumi consumers (a Terraform stack, a Kubernetes manifest pipeline, an external CD system) but loses the dependency tracking. Use StackReference whenever both ends are Pulumi; reach for files only at the boundary with other tools.
| Pattern | Dependency tracking | Secret handling | When to use |
|---|---|---|---|
StackReference | Yes | Decrypted automatically | Pulumi-to-Pulumi consumption |
--json file passed as config | No (manual) | --show-secrets required | Bridging to non-Pulumi tools |
| Pulumi ESC environment | Indirect (via config) | Built-in | Shared secrets across many stacks |
Pulumi config secrets vs raw JSON values
Pulumi config lives in Pulumi.<stack>.yaml — one file per stack (dev, staging, prod). Set values with pulumi config set key value; encrypt sensitive ones with pulumi config set --secret key value. The encrypted form lands in the file as a secure:-prefixed cipher block; the underlying secret never appears in plaintext on disk.
# Pulumi.prod.yaml after pulumi config set commands
config:
aws:region: us-east-1
myapp:domainName: api.example.com
myapp:dbHost: prod-db.acme.internal
myapp:dbPassword:
secure: AAABAJ+xQ7xZ...vEqK4Jm4=
myapp:featureFlags:
enabled:
- billing-v2
- new-searchInspect the full config tree as JSON with pulumi config get --json or pulumi config --json. The JSON form preserves nested structure (objects and arrays) and marks secret values with a secret: truesibling field — the actual ciphertext only decrypts inside a Pulumi process that has the stack's passphrase or KMS key. Never log decrypted values; they leak into CI artifacts forever.
$ pulumi config --json
{
"aws:region": { "value": "us-east-1" },
"myapp:domainName": { "value": "api.example.com" },
"myapp:dbHost": { "value": "prod-db.acme.internal" },
"myapp:dbPassword": { "value": "[secret]", "secret": true },
"myapp:featureFlags": {
"value": "{\"enabled\":[\"billing-v2\",\"new-search\"]}",
"objectValue": { "enabled": ["billing-v2", "new-search"] }
}
}For structured config, use config.requireObject<T>() in TypeScript or config.require_object() in Python — Pulumi parses the YAML/JSON sub-document into a typed value. See our JSON config patterns guide for the broader question of when to use JSON vs YAML vs TOML for application config.
Pulumi vs CloudFormation vs Terraform: JSON surface comparison
All three tools deal in JSON at some layer, but the role of JSON differs in each. CloudFormation is JSON-first — the template format itself is JSON (or YAML, which compiles to the same internal representation). Terraform writes its primary configuration in HCL but persists state as JSON and can ingest JSON-form HCL. Pulumi programs are written in real languages, with JSON appearing only at boundaries.
| Surface | Pulumi | CloudFormation | Terraform |
|---|---|---|---|
| Primary program format | TS/Py/Go/.NET/Java | JSON or YAML template | HCL (or JSON-form HCL) |
| State storage format | JSON checkpoint | Service-managed (opaque) | JSON state file |
| Outputs to CI | stack output --json | describe-stacks --query Stacks[0].Outputs | terraform output -json |
| Embedded IAM policy | Native object → JSON.stringify | Inline JSON in template | HCL block or jsonencode |
| Cross-stack reference | StackReference | Export/Fn::ImportValue | terraform_remote_state |
| Drift detection | pulumi refresh | detect-stack-drift | terraform plan -refresh-only |
| Comments in primary config | Yes (real language) | YAML form only | HCL form only |
The practical implication: if your team already lives in JSON (you generate templates, you transform them with jq, you store them in a CMDB), Pulumi YAML in JSON form gives you that workflow with proper resource graph semantics underneath. If you live in a programming language, the JSON shows up only at the four boundaries this guide covers — state, outputs, config, and embedded policy strings.
For the full migration story from each tool, see the CloudFormation comparison, the Azure ARM JSON comparison, and the Terraform comparison.
Key terms
- stack
- A named instance of a Pulumi project — typically one per environment (dev, staging, prod). Each stack has its own state checkpoint, config file (
Pulumi.<stack>.yaml), and set of outputs. - checkpoint
- The JSON document Pulumi uses to record the current state of a stack: every resource, its inputs and outputs, the dependency graph, and metadata about the run that produced it. Stored in whichever backend the project is configured to use.
- StackReference
- A first-class Pulumi resource that reads outputs from another stack's checkpoint. Returns typed Output values that participate in the dependency graph, so changes upstream schedule updates downstream automatically.
- Pulumi YAML
- A declarative programming language for Pulumi, released in 2022. Accepts both YAML and JSON form. Suited to flat resource lists; lacks the loops, conditionals, and functions you would write in TypeScript or Python.
- Output
- A Pulumi runtime type representing a value that may not exist yet — a cloud resource ARN, an autogenerated ID, an endpoint URL. Outputs participate in the dependency graph and resolve to real values during
pulumi up. - backend
- Where Pulumi stores stack checkpoints: Pulumi Cloud (default), AWS S3, Azure Blob, Google Cloud Storage, or a local filesystem path. The JSON format is identical across all backends; the difference is locking, versioning, and access control.
- secret config
- A config value encrypted at rest in
Pulumi.<stack>.yamlusing a passphrase or cloud KMS key. The stored ciphertext begins withsecure:; the plaintext only appears inside a Pulumi process with the matching key.
Frequently asked questions
Where does Pulumi store stack state JSON?
Pulumi stores stack state as a JSON checkpoint file in whichever backend you configured: Pulumi Cloud (the default, managed at app.pulumi.com), AWS S3 (s3://my-bucket?region=us-east-1), Azure Blob (azblob://my-container), Google Cloud Storage (gs://my-bucket), or a local filesystem path (file://~/.pulumi). The on-disk layout is the same regardless of backend — a checkpoint JSON document containing the resource graph, all output values (with secrets encrypted), the version of the Pulumi engine that wrote it, and metadata about the most recent operation. Run pulumi stack export --file state.json to pull the current state down to a local file you can inspect, version, or diff against an earlier snapshot. Local backends store the same file directly under .pulumi/stacks/<stack>.json — useful for offline development but unsafe for teams because there is no locking.
How do I read Pulumi outputs as JSON in a CI pipeline?
After pulumi up succeeds, run pulumi stack output --json to dump every exported output as a JSON object on stdout. Redirect to a file (pulumi stack output --json > outputs.json) or pipe directly into jq for selective extraction (pulumi stack output --json | jq -r .apiUrl). The --json flag returns the full structure even for nested objects and arrays — without it, complex outputs print a Go-style map representation that is painful to parse. To include secret values (encrypted at rest in the checkpoint), add --show-secrets, but never log that output in CI artifacts. In GitHub Actions, capture the JSON into a step output (echo "outputs=$(pulumi stack output --json)" >> $GITHUB_OUTPUT) and reference fields with fromJson() in downstream steps. This is the standard contract for handing Pulumi-generated identifiers (cluster names, queue URLs, IAM role ARNs) to subsequent deploy or test stages.
Can I write Pulumi programs in JSON?
Partially yes. Pulumi YAML (released as a first-class language in 2022) accepts both YAML and JSON forms — any valid JSON document is also valid YAML, so a Pulumi.yaml written in JSON syntax will run unchanged. The pulumi-language-yaml plugin parses either form into the same resource graph and submits it to the engine. This is useful when a generator (CDK8s, an internal templating tool, or a Terraform-to-Pulumi converter) emits JSON natively and you want to skip the YAML formatting step. The caveat: Pulumi YAML is a declarative subset of the full programming-language SDKs. You get resources, variables, outputs, and a few control-flow primitives, but no loops, conditionals, or functions of the kind you would write in TypeScript or Python. For complex infrastructure, stick with a real programming language; JSON-form YAML is best for simple, declarative stacks.
How do I include a JSON IAM policy file in Pulumi?
Three patterns work depending on the language. First, inline literal: in TypeScript or Python, write the policy as a native object (JSON.stringify or json.dumps it when the AWS resource expects a string field). The AWS provider accepts native objects directly for fields like policy and assumeRolePolicy and serializes them for you. Second, read from a file: import fs and JSON.parse the contents of policy.json at program load time — fine for static policies that ship with the repo. Third, use the aws.iam.getPolicyDocument data source, which builds the JSON from a structured input and validates the result against the IAM policy grammar at preview time. The data-source approach catches typos (mis-spelled Action keys, malformed ARN patterns) before Pulumi sends the request to AWS, which the literal-string approach does not. For policies you reuse across stacks, define them once in a helpers module and import.
What's the difference between pulumi.yaml and Pulumi YAML programs?
Pulumi.yaml (the project file) and Pulumi YAML (the programming language) are different things that share a name. Every Pulumi project — TypeScript, Python, Go, anything — has a Pulumi.yaml at the project root that declares metadata: project name, runtime, description, plugins. There is also a per-stack Pulumi.<stack>.yaml file holding stack-specific configuration values for that environment (dev, staging, prod). Pulumi YAML the language is a separate thing — a declarative YAML-or-JSON schema for writing the actual infrastructure code, used when the runtime field in Pulumi.yaml is set to yaml. So a TypeScript project has Pulumi.yaml plus index.ts; a Pulumi YAML project has Pulumi.yaml that itself contains the resource definitions inline (runtime: yaml). The naming is unfortunate but the distinction is unambiguous in practice.
How do I migrate from CloudFormation JSON to Pulumi?
Two routes. The mechanical one is pulumi import — point it at an existing CloudFormation-managed resource (or an entire stack via the CloudControl provider) and Pulumi reads the current state from AWS, generates equivalent code in your chosen language, and writes the resource into the Pulumi state file. The semantic one is rewriting the CloudFormation template by hand: for each AWS::* type, find the corresponding @pulumi/aws or @pulumi/aws-native resource and translate the properties. The native provider (@pulumi/aws-native) maps one-to-one to CloudFormation resource types so the translation is almost mechanical, while the classic @pulumi/aws provider gives nicer ergonomics but does not cover every CloudFormation property. For mixed migrations, use the aws-native provider as the primary, drop down to the classic provider for the handful of resources where it has better defaults, and run pulumi import in batches to avoid one giant preview diff.
How do I parse pulumi state JSON for inventory reports?
Run pulumi stack export --file state.json to get the raw checkpoint, then walk the deployment.resources array — each entry has a type (aws:s3/bucket:Bucket, aws:iam/role:Role, etc.), a urn (the unique Pulumi identifier), an outputs object containing the live AWS values, and a parent reference for component hierarchy. For a flat inventory CSV, pipe through jq: pulumi stack export | jq -r .deployment.resources[] | [.type, .urn, .outputs.arn] | @csv. For richer reports, parse the JSON in Python or Node and join against billing data or a CMDB. The checkpoint format is documented and stable across minor versions of the Pulumi engine — automation that reads it is safe for production use. Treat the file as sensitive: it contains every output value, including ones marked as secrets (those are encrypted), and it leaks the shape of your infrastructure to anyone with read access.
Can I share JSON outputs between Pulumi stacks?
Yes — that is what StackReference is for. In a downstream stack, instantiate new pulumi.StackReference("org/project/upstream-stack") and call getOutput("name") to pull a specific output (or requireOutput if it must exist). The reference reads the upstream checkpoint via the configured backend and surfaces outputs as Pulumi Output<T> values, so they participate in the dependency graph and update events flow correctly when the upstream changes. For non-Pulumi consumers (a Terraform stack, a Kubernetes manifest pipeline, an external CI step), use pulumi stack output --json on the upstream and feed the result downstream as a file or environment variable. StackReference is preferable when both sides are Pulumi because it preserves the secret-encryption status of values and gives proper dependency tracking; the --json file approach is the universal escape hatch.
Further reading and primary sources
- Pulumi Docs — Stack Outputs — Reference for exporting values from a stack and reading them via the CLI
- Pulumi Docs — State and Backends — How checkpoint files are stored across Pulumi Cloud, S3, Azure Blob, and GCS backends
- Pulumi YAML Language Reference — The declarative YAML/JSON programming language for Pulumi, with the full schema
- Pulumi StackReference — Cross-stack consumption with dependency tracking and secret decryption
- Pulumi import command — Bring existing CloudFormation, Terraform, or hand-built resources under Pulumi management