Embed Pulumi in a FastAPI Service with the Automation API

The Pulumi Automation API turns pulumi up into a library call, which means you can drive deployments from an HTTP service instead of a CLI. This guide — under Pulumi patterns and provider management — builds a small FastAPI endpoint that provisions a per-tenant stack on demand, the pattern behind self-service infrastructure portals.

Why This Matters

A CLI is fine for engineers, but product teams often need infrastructure created in response to an event: a new customer signs up, a preview environment is requested, a batch job needs a bucket. Wrapping programmatic deployments in an API lets those events trigger real, stateful deployments with full preview and rollback.

Request to deployment Request to deployment: Client → FastAPI → Automation API → Cloud. Client FastAPI Automation API Cloud POST /envs up() create outputs result 202 + id
The API accepts the request, runs the stack asynchronously, and returns a job id the client polls.

The key design constraint is that deployments are slow and stateful, so the HTTP layer must treat them as background work, not a synchronous request you block on.

Prerequisites

Prerequisites Prerequisites: layered from fastapi down to Cloud. fastapi uvicorn Automation API Cloud
Prerequisites: the building blocks this section assembles.
  • pulumi>=3.0 and fastapi, uvicorn installed in the service virtualenv
  • The Pulumi CLI on the host (the Automation API shells out to it) and a configured state backend
  • Cloud credentials available to the service process via its execution role, not baked into the image
# CLI: the Automation API needs the pulumi binary on PATH
pulumi version && python -c "from pulumi.automation import create_or_select_stack"

Defining an Inline Program

The Automation API can run an inline program — a Python function instead of a project directory — which is ideal for embedding. Each tenant gets its own stack name so state stays isolated.

Service architecture Service architecture: layered from FastAPI endpoint (async) down to Cloud provider. FastAPI endpoint (async) Automation API driver Inline Pulumi program Per-tenant state backend Cloud provider
Each layer isolates concerns from the HTTP boundary down to the cloud.
# service.py — a FastAPI endpoint that provisions a per-tenant bucket
# CLI: uvicorn service:app --port 8080
from fastapi import FastAPI, BackgroundTasks
from pulumi import automation as auto
import pulumi_aws as aws

app = FastAPI()

def _program(tenant: str):
    def build():
        bucket = aws.s3.BucketV2(f"{tenant}-data")
        return {"bucket": bucket.bucket}
    return build

def _deploy(tenant: str) -> None:
    stack = auto.create_or_select_stack(
        stack_name=f"tenant-{tenant}",
        project_name="tenant-envs",
        program=_program(tenant),
    )
    stack.workspace.install_plugin("aws", "v6.0.0")  # Provider note: pin the plugin version
    stack.set_config("aws:region", auto.ConfigValue("us-east-1"))
    stack.up(on_output=print)  # State implication: writes to the tenant's own state file

@app.post("/envs/{tenant}", status_code=202)
def create_env(tenant: str, bg: BackgroundTasks):
    bg.add_task(_deploy, tenant)
    return {"status": "accepted", "stack": f"tenant-{tenant}"}

Verification

Exercise the endpoint and confirm a stack and state file appear, then destroy it to prove the reverse path works.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# CLI: trigger a deployment and inspect the resulting stack
curl -X POST localhost:8080/envs/acme
pulumi stack ls          # expect: tenant-acme listed
pulumi -s tenant-acme destroy --yes

Gotchas & Edge Cases

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 4 facets. Where it breaks Credentials auth & region State lock & drift Types schema mismatch Ordering dependency graph
Gotchas & Edge Cases: the boundaries where things break and what to check.

Concurrency corrupts state. Two overlapping up() calls on the same stack will collide on the state lock. Serialise per-stack work with a queue or a per-stack lock; never run two deployments of the same stack at once.

Long deployments outlive requests. A 30-second HTTP timeout will kill a deployment mid-flight if you run it synchronously. Always use background tasks or a worker queue and return a job id.

Plugin installs are slow. install_plugin downloads on first run; pre-bake plugins into the container image so cold starts do not time out.

FAQ

Can I preview instead of deploying from the API?

Yes — call stack.preview() and return the change summary. This is how self-service portals show a plan before a human approves the real up().

Where does state live for inline programs?

In whatever backend the workspace is configured for. Point it at S3 or Pulumi Cloud so multiple service replicas share state, as discussed in choosing a state backend.

Is the Automation API the same as the CLI?

It drives the same engine and state, so a stack created via the API is fully manageable from the CLI and vice versa.