Triggering Dependent Stack Updates
When you split your infrastructure across multiple stacks, an upstream stack often exposes outputs that downstream stacks consume through a stack reference — for example, a network stack whose VPC ID feeds a database stack, which in turn feeds a compute stack. A stack reference reads the current outputs of the upstream stack, but it does not re-run the downstream stack on its own. When the upstream stack changes, you need a way to cascade an update to the stacks that depend on it.
The supported way to do this is with Pulumi Deployments: configure the downstream stack to run automatically when the upstream stack’s update succeeds. This page covers the recommended pattern, how to keep it from firing duplicate or looping deployments, and why to avoid rolling your own webhook.
The recommended pattern
Trigger each downstream stack from the upstream stack’s update_succeeded event, using Pulumi Deployments. There are two supported ways to wire this up:
- Deployment webhook destinations — declare one webhook per upstream → downstream edge.
- The Pulumi Auto Deploy package — declare the whole dependency graph and let it manage the webhooks for you.
These dependencies usually follow one of two patterns:
- Layered infrastructure within an environment: stacks in the same environment build on one another — a
networkstack feeds adatabasestack and aclusterstack, which in turn feed anappstack. Updating a lower layer cascades to the layers above it. - Promoting a change across environments: the same layer flows from one environment to the next — updating
network/devpromotes tonetwork/staging, which promotes tonetwork/prod.
Both patterns are directed acyclic graphs, and either approach below can express them.
Both approaches require that every stack you want to deploy automatically is configured with deployment settings, since the trigger runs the target stack through Pulumi Deployments.
The examples on this page use the Pulumi Service provider and Pulumi IaC to manage these webhooks declaratively, which is the recommended approach — your dependency wiring lives in code, is reviewed, and stays in sync with the rest of your infrastructure. You can also configure the same webhooks through the Pulumi Cloud console or the REST API if you prefer.
pulumi destroy of) your application infrastructure to also tear down the webhooks that keep your dependent stacks up to date, and vice versa.Deployment webhook destinations
A deployment webhook destination lets you pick one or more event types on a stack (for example, update_succeeded) and deliver the event to a destination — in this case, the Create Deployment API of another stack. In the example below, an update to the network stack triggers the database stack, and an update to the database stack triggers the compute stack.
import * as pulumi from "@pulumi/pulumi";
import * as pulumiservice from "@pulumi/pulumiservice";
const databaseWebhook = new pulumiservice.Webhook("databaseWebhook", {
organizationName: "org",
projectName: "network",
stackName: "prod",
format: pulumiservice.WebhookFormat.PulumiDeployments,
payloadUrl: "database/prod",
active: true,
displayName: "deploy-database",
filters: [pulumiservice.WebhookFilters.UpdateSucceeded],
});
const computeWebhook = new pulumiservice.Webhook("computeWebhook", {
organizationName: "org",
projectName: "database",
stackName: "prod",
format: pulumiservice.WebhookFormat.PulumiDeployments,
payloadUrl: "compute/prod",
active: true,
displayName: "deploy-compute",
filters: [pulumiservice.WebhookFilters.UpdateSucceeded],
});
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using PulumiService = Pulumi.PulumiService;
return await Deployment.RunAsync(() =>
{
var databaseWebhook = new PulumiService.Webhook("databaseWebhook", new()
{
OrganizationName = "org",
ProjectName = "network",
StackName = "prod",
Format = PulumiService.WebhookFormat.PulumiDeployments,
PayloadUrl = "database/prod",
Active = true,
DisplayName = "deploy-database",
Filters = new[]
{
PulumiService.WebhookFilters.UpdateSucceeded,
},
});
var computeWebhook = new PulumiService.Webhook("computeWebhook", new()
{
OrganizationName = "org",
ProjectName = "database",
StackName = "prod",
Format = PulumiService.WebhookFormat.PulumiDeployments,
PayloadUrl = "compute/prod",
Active = true,
DisplayName = "deploy-compute",
Filters = new[]
{
PulumiService.WebhookFilters.UpdateSucceeded,
},
});
});
import pulumi
import pulumi_pulumiservice as pulumiservice
database_webhook = pulumiservice.Webhook("databaseWebhook",
organization_name="org",
project_name="network",
stack_name="prod",
format=pulumiservice.WebhookFormat.PULUMI_DEPLOYMENTS,
payload_url="database/prod",
active=True,
display_name="deploy-database",
filters=[pulumiservice.WebhookFilters.UPDATE_SUCCEEDED])
compute_webhook = pulumiservice.Webhook("computeWebhook",
organization_name="org",
project_name="database",
stack_name="prod",
format=pulumiservice.WebhookFormat.PULUMI_DEPLOYMENTS,
payload_url="compute/prod",
active=True,
display_name="deploy-compute",
filters=[pulumiservice.WebhookFilters.UPDATE_SUCCEEDED])
package main
import (
"github.com/pulumi/pulumi-pulumiservice/sdk/go/pulumiservice"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := pulumiservice.NewWebhook(ctx, "databaseWebhook", &pulumiservice.WebhookArgs{
OrganizationName: pulumi.String("org"),
ProjectName: pulumi.String("network"),
StackName: pulumi.String("prod"),
Format: pulumiservice.WebhookFormatPulumiDeployments,
PayloadUrl: pulumi.String("database/prod"),
Active: pulumi.Bool(true),
DisplayName: pulumi.String("deploy-database"),
Filters: pulumiservice.WebhookFiltersArray{
pulumiservice.WebhookFiltersUpdateSucceeded,
},
})
if err != nil {
return err
}
_, err = pulumiservice.NewWebhook(ctx, "computeWebhook", &pulumiservice.WebhookArgs{
OrganizationName: pulumi.String("org"),
ProjectName: pulumi.String("database"),
StackName: pulumi.String("prod"),
Format: pulumiservice.WebhookFormatPulumiDeployments,
PayloadUrl: pulumi.String("compute/prod"),
Active: pulumi.Bool(true),
DisplayName: pulumi.String("deploy-compute"),
Filters: pulumiservice.WebhookFiltersArray{
pulumiservice.WebhookFiltersUpdateSucceeded,
},
})
if err != nil {
return err
}
return nil
})
}
name: auto-deploy-demo
runtime: yaml
description: A simple auto-deploy example
resources:
databaseWebhook:
type: pulumiservice:Webhook
properties:
organizationName: org
projectName: network
stackName: prod
format: pulumi_deployments
payloadUrl: database/prod
active: true
displayName: deploy-database
filters:
- update_succeeded
computeWebhook:
type: pulumiservice:Webhook
properties:
organizationName: org
projectName: database
stackName: prod
format: pulumi_deployments
payloadUrl: compute/prod
active: true
displayName: deploy-compute
filters:
- update_succeeded
Pulumi Auto Deploy
The Pulumi Auto Deploy package lets you declaratively express dependencies between stacks in a Pulumi program and manages the necessary deployment webhooks for you. Because you describe the graph as a set of downstreamRefs, Auto Deploy keeps the wiring consistent and acyclic as your dependencies grow — you don’t hand-maintain one webhook per edge.
The following example models the layered-infrastructure pattern: a network stack feeds a database stack and a cluster stack, both of which feed an app stack. Each AutoDeployer names a stack and lists the stacks that should update when it changes.
flowchart TD
network --> database
network --> cluster
database --> app
cluster --> app
import * as autodeploy from "@pulumi/auto-deploy";
import * as pulumi from "@pulumi/pulumi";
// Layered infrastructure in a single environment: network -> database, cluster -> app.
// Updating a stack automatically updates every stack downstream of it via a webhook
// that triggers Pulumi Deployments.
const organization = pulumi.getOrganization();
const stack = "prod";
// The application sits at the top of the graph and has no downstream stacks.
export const app = new autodeploy.AutoDeployer("app", {
organization,
project: "app",
stack,
downstreamRefs: [],
});
// The database and the cluster each feed the application.
export const database = new autodeploy.AutoDeployer("database", {
organization,
project: "database",
stack,
downstreamRefs: [app.ref],
});
export const cluster = new autodeploy.AutoDeployer("cluster", {
organization,
project: "cluster",
stack,
downstreamRefs: [app.ref],
});
// The network underpins everything; updating it updates the database and the cluster.
export const network = new autodeploy.AutoDeployer("network", {
organization,
project: "network",
stack,
downstreamRefs: [database.ref, cluster.ref],
});
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AutoDeploy = Pulumi.AutoDeploy;
// Layered infrastructure in a single environment: network -> database, cluster -> app.
// Updating a stack automatically updates every stack downstream of it via a webhook
// that triggers Pulumi Deployments.
return await Deployment.RunAsync(() =>
{
var organization = "pulumi";
var stack = "prod";
// The application sits at the top of the graph and has no downstream stacks.
var app = new AutoDeploy.AutoDeployer("app", new()
{
Organization = organization,
Project = "app",
Stack = stack,
DownstreamRefs = new[] {},
});
// The database and the cluster each feed the application.
var database = new AutoDeploy.AutoDeployer("database", new()
{
Organization = organization,
Project = "database",
Stack = stack,
DownstreamRefs = new[] { app.Ref },
});
var cluster = new AutoDeploy.AutoDeployer("cluster", new()
{
Organization = organization,
Project = "cluster",
Stack = stack,
DownstreamRefs = new[] { app.Ref },
});
// The network underpins everything; updating it updates the database and the cluster.
var network = new AutoDeploy.AutoDeployer("network", new()
{
Organization = organization,
Project = "network",
Stack = stack,
DownstreamRefs = new[]
{
database.Ref,
cluster.Ref,
},
});
});
import pulumi
import pulumi_auto_deploy as auto_deploy
# Layered infrastructure in a single environment: network -> database, cluster -> app.
# Updating a stack automatically updates every stack downstream of it via a webhook
# that triggers Pulumi Deployments.
organization = pulumi.get_organization()
stack = "prod"
# The application sits at the top of the graph and has no downstream stacks.
app = auto_deploy.AutoDeployer("app",
organization=organization,
project="app",
stack=stack,
downstream_refs=[])
# The database and the cluster each feed the application.
database = auto_deploy.AutoDeployer("database",
organization=organization,
project="database",
stack=stack,
downstream_refs=[app.ref])
cluster = auto_deploy.AutoDeployer("cluster",
organization=organization,
project="cluster",
stack=stack,
downstream_refs=[app.ref])
# The network underpins everything; updating it updates the database and the cluster.
network = auto_deploy.AutoDeployer("network",
organization=organization,
project="network",
stack=stack,
downstream_refs=[
database.ref,
cluster.ref,
])
package main
import (
"github.com/pulumi/pulumi-auto-deploy/sdk/go/autodeploy"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
// Layered infrastructure in a single environment: network -> database, cluster -> app.
// Updating a stack automatically updates every stack downstream of it via a webhook
// that triggers Pulumi Deployments.
pulumi.Run(func(ctx *pulumi.Context) error {
organization := "pulumi"
stack := "prod"
// The application sits at the top of the graph and has no downstream stacks.
app, err := autodeploy.NewAutoDeployer(ctx, "app", &autodeploy.AutoDeployerArgs{
Organization: pulumi.String(organization),
Project: pulumi.String("app"),
Stack: pulumi.String(stack),
DownstreamRefs: pulumi.StringArray{},
})
if err != nil {
return err
}
// The database and the cluster each feed the application.
database, err := autodeploy.NewAutoDeployer(ctx, "database", &autodeploy.AutoDeployerArgs{
Organization: pulumi.String(organization),
Project: pulumi.String("database"),
Stack: pulumi.String(stack),
DownstreamRefs: pulumi.StringArray{
app.Ref,
},
})
if err != nil {
return err
}
cluster, err := autodeploy.NewAutoDeployer(ctx, "cluster", &autodeploy.AutoDeployerArgs{
Organization: pulumi.String(organization),
Project: pulumi.String("cluster"),
Stack: pulumi.String(stack),
DownstreamRefs: pulumi.StringArray{
app.Ref,
},
})
if err != nil {
return err
}
// The network underpins everything; updating it updates the database and the cluster.
_, err = autodeploy.NewAutoDeployer(ctx, "network", &autodeploy.AutoDeployerArgs{
Organization: pulumi.String(organization),
Project: pulumi.String("network"),
Stack: pulumi.String(stack),
DownstreamRefs: pulumi.StringArray{
database.Ref,
cluster.Ref,
},
})
if err != nil {
return err
}
return nil
})
}
name: auto-deploy-demo
runtime: yaml
description: Layered infrastructure with automatic dependent-stack updates
variables:
stack: prod
resources:
# The application sits at the top of the graph and has no downstream stacks.
app:
type: auto-deploy:AutoDeployer
properties:
organization: ${pulumi.organization}
project: app
stack: ${stack}
downstreamRefs: []
# The database and the cluster each feed the application.
database:
type: auto-deploy:AutoDeployer
properties:
organization: ${pulumi.organization}
project: database
stack: ${stack}
downstreamRefs:
- ${app.ref}
cluster:
type: auto-deploy:AutoDeployer
properties:
organization: ${pulumi.organization}
project: cluster
stack: ${stack}
downstreamRefs:
- ${app.ref}
# The network underpins everything; updating it updates the database and the cluster.
network:
type: auto-deploy:AutoDeployer
properties:
organization: ${pulumi.organization}
project: network
stack: ${stack}
downstreamRefs:
- ${database.ref}
- ${cluster.ref}
Choosing between the two approaches
Both approaches use the same underlying mechanism — a deployment webhook that runs the downstream stack when the upstream stack’s update succeeds. Choose based on how you prefer to manage the wiring:
- Deployment webhook destinations give you explicit, per-edge control. They are a good fit when you have a small number of dependencies or want to manage each webhook individually.
- The Pulumi Auto Deploy package lets you declare the whole dependency graph in one place and manages the webhooks for you. It is a good fit when you have many stacks or a graph that changes over time, and declaring the graph in one place makes it easier to keep acyclic.
Avoiding duplicate fires and re-trigger loops
The most common problems people hit when cascading deployments are the same stack deploying several times for a single upstream change, or two stacks re-triggering each other indefinitely. The supported pattern avoids both when you follow these rules:
- Filter on a single terminal event. Trigger on
update_succeededonly. A single deployment emits several events over its lifecycle —deployment_queued,deployment_started, and finallyupdate_succeeded— so subscribing to more than one event kind (or to lifecycle events instead of the terminal one) causes the downstream stack to fire multiple times per upstream change. - Target the downstream stack’s deployment directly. Use the built-in Deployment webhook destination, which calls the Create Deployment API of a specific stack. This is a directed edge with a known endpoint, unlike a generic HTTP webhook pointed at custom automation that runs
pulumi upand can fan out to more work than you intended. - Keep the dependency graph acyclic. Never let stack A trigger stack B while stack B triggers stack A, whether directly or through a longer chain. A cycle means each update triggers the next one forever. Model your stacks as a directed acyclic graph (DAG); declaring the whole graph in one place with the Pulumi Auto Deploy package helps you keep this shape.
What to avoid: the naive webhook workaround
A tempting shortcut is to attach a generic webhook to the upstream stack that points at a custom endpoint — a serverless function or a CI job — which then shells out to pulumi up on the downstream stack. Avoid this. In practice it tends to:
- Fire far more often than intended, because a generic webhook that isn’t narrowly filtered receives every event kind for the stack, so one upstream change can kick off many downstream runs.
- Re-trigger in loops, because custom automation has no built-in awareness of the dependency graph and nothing stops a downstream update from triggering the upstream one again.
- Duplicate deployment logic outside Pulumi, reimplementing credentials, source configuration, and settings that deployment settings already manage.
Use a deployment webhook destination or the Pulumi Auto Deploy package instead. Both run the target stack through Pulumi Deployments with its saved settings, so the trigger stays scoped to one deployment per upstream change.
Triggering with custom or one-off settings
A deployment webhook runs the target stack with its existing deployment settings and cannot override them. When you need to trigger a downstream deployment with custom or one-off settings — a different branch, environment variables, or pre-run commands — use the REST API trigger instead, which accepts settings in the request body.
Related
- Deployment triggers — every way a deployment can be initiated.
- Deployment webhooks — the webhook and event-filtering reference.
- Deployment settings — required on any stack you deploy automatically.
- Stack references — how downstream stacks consume upstream outputs.
- Pulumi Auto Deploy package — declarative dependent-stack updates.