Pulumi Cloud Webhooks
Pulumi Webhooks allow you to notify external services of events
happening within your Pulumi organization. For example,
you can trigger a notification whenever a stack is updated.
When an event occurs, Pulumi will notify the registered webhook listeners via a HTTP POST
request with metadata about the event. The webhook can then be used to emit a
notification, start running integration tests, or even update additional stacks.
There are large number of real life applications for webhooks including serving as the foundation of most ChatOps workflows.
Overview
Pulumi Cloud webhooks can be attached to either a stack or an organization. Stack webhooks will be notified of events specific to the stack. Organization webhooks will be notified for events happening within each of the organization’s stacks.
The Webhooks page is under the Stack or Organization Settings tab.
If you are looking for Environment Webhook documentation, it’s here.
Create a Webhook
Pulumi Webhooks may be created through the UI using the steps outlined below, by using the Webhook resource from the Pulumi provider or by using the API directly.
import * as pulumi from "@pulumi/pulumi";
import * as pulumiservice from "@pulumi/pulumiservice";
const webhook = new pulumiservice.Webhook("example-webhook", {
active: true,
displayName: "webhook example",
organizationName: "example",
payloadUrl: "https://example.com/webhook",
});
import pulumi
import pulumi_pulumiservice
webhook = pulumi_service.Webhook("example-webhook",
active: True,
display_name: "webhook example",
organization_name: "example",
payload_url: "https://example.com/webhook",
)
import (
"fmt"
"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 {
webhook, err := pulumiservice.NewWebhook(ctx, "example-webhook", &pulumiservice.WebhookArgs{
Active: pulumi.Bool(true),
DisplayName: pulumi.String("example webhook"),
OrganizationName: pulumi.String("example"),
PayloadURL: pulumi.String("https://example.com/webhook"),
}, nil)
if err != nil {
return fmt.Errorf("error creating webhook: %v", err)
}
return nil
})
}
using Pulumi;
using Pulumi.PulumiService;
class PulumiServiceWebhook: Stack
{
public PulumiServiceWebhook()
{
var webhook = new Webhook("example-webhook", new WebhookArgs{
Active = true,
DisplayName = "example webhook",
OrganizationName = "example",
PayloadUrl = "https://example.com/webhook"
})
}
}
Create an Organization Webhook
- Navigate to Settings > Webhooks.
- Select Create webhook.
- Under Destination, choose Webhook, Slack or Microsoft Teams.
- For generic JSON webhooks, provide a display name, payload URL, and optionally a secret.
- For Slack webhooks, provide a Slack webhook URL and a display name.
- For Microsoft Teams webhooks, provide a Microsoft Teams webhook URL and a display name.
- Choose which events you would like to receive using groups and filters menu.
Create a Stack Webhook
- Navigate to the stack.
- Navigate to Settings > Webhooks
- Select Create webhook.
- Under Destination, choose Webhook, Slack, Microsoft Teams or Deployment.
- For generic JSON webhooks, provide a display name, payload URL, and optionally a secret.
- For Slack webhooks, provide a Slack webhook URL and a display name.
- For Microsoft Teams webhooks, provide a Microsoft Teams webhook URL and a display name.
- For Deployment webhooks, provide the stack to deploy in the format
project/stack
.
- Choose which events you would like to receive using groups and filters menu.
Event Filtering
Event filtering allows you to choose which events should be delivered to each webhook. You may choose to receive all events in a group, or filter to specific events (only failures, only deployment events, etc.). The following table describes the various event filters available and the context in which they are relevant.
Filter | Event Kind | Webhook Type | Triggered |
---|---|---|---|
stack_created | stack | Organization webhooks only | When a stack is created. |
stack_deleted | stack | Organization webhooks only | When a stack is deleted. |
preview_succeeded | stack_preview | Organization or Stack | When a stack preview succeeds. |
preview_failed | stack_preview | Organization or Stack | When a stack preview fails. |
update_succeeded | stack_update | Organization or Stack | When a stack update succeeds. |
update_failed | stack_update | Organization or Stack | When a stack update fails. |
destroy_succeeded | stack_update | Organization or Stack | When a stack destroy succeeds. |
destroy_failed | stack_update | Organization or Stack | When a stack destroy fails. |
refresh_succeeded | stack_update | Organization or Stack | When a stack refresh succeeds. |
refresh failed | stack_update | Organization or Stack | When a stack refresh fails. |
deployment_queued | deployment | Organization or Stack | When a deployment is queued. |
deployment_started | deployment | Organization or Stack | When a deployment starts running. |
deployment_succeeded | deployment | Organization or Stack | When a deployment succeeds. |
deployment_failed | deployment | Organization or Stack | When a deployment fails. |
drift_detected | drift_detection | Organization or Stack | When drift is detected in a drift detection run. |
drift_detection_succeeded | drift_detection | Organization or Stack | When a drift detection run succeeds. |
drift_detection_failed | drift_detection | Organization or Stack | When a drift detection run fails. |
drift_remediation_succeeded | drift_remediation | Organization or Stack | When a drift remediation run succeeds. |
drift_remediation_failed | drift_remediation | Organization or Stack | When a drift remediation run fails. |
policy_violation_mandatory | policy_violation | Organization or Stack | When a mandatory policy violation is detected. |
policy_violation_advisory | policy_violation | Organization or Stack | When an advisory policy violation is detected. |
And this table describes the various filter groups available to easily subscribe to all events within a group.
Group | Event Kinds Included |
---|---|
stacks | stack , stack_preview , stack_update |
deployments | deployment , drift_detection , drift_remediation |
Webhook Formats
When creating a webhook, you can choose between a generic JSON webhook payload, slack
formatted events and ms_teams
formatted events.
Slack Webhooks
Slack Webhooks allow you to seamlessly integrate notifications about your Pulumi resources into your Slack workspace by simply providing a Slack incoming webhook URL and optionally choosing which events you want delivered using event groups and filters.
You can either create your own Slack app (or use an existing one you may already have installed in your workspace), or follow the link below to quickly get started with a pre-defined Slack app manifest.
Microsoft Teams Webhooks
Microsoft Teams Webhooks allow you to seamlessly integrate notifications about your Pulumi stacks and organizations into your Microsoft Teams workspace by simply providing a Microsoft Teams incoming webhook workflow URL and optionally choosing which events you want delivered using event groups and filters.
Deployment Webhooks
The Deployment webhook destination lets you trigger updates on other stacks via Pulumi Deployments, usually in response to update_succeeded
events. This enables you to keep dependent stacks up to date automatically which is often necessary when using stack references.
Deployment webhooks require that your stacks are configured with Deployment Settings.
Generic JSON Webhooks
When using generic JSON webhooks, Pulumi will send an HTTP POST
request to
all registered webhooks. The webhook can then be used to emit a
notification, start running integration tests, or even update additional stacks.
Payload Examples
Each webhook payload has a format specific to the payload being emitted. Every payload will contain a sender, organization, and stack reference as appropriate. For examples of specific payloads, see Payload Reference below.
Each webhook will contain an organization
field, which is the organization name, and a URL for the event. It may also contain the user
who requested the action, as well as the projectName
and stackName
when applicable.
Stack Creation
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"action": "created",
"projectName": "website",
"stackName": "website-prod"
}
Stack Update
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"projectName": "website",
"stackName": "website-prod",
"updateUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/updates/42",
"kind": "refresh",
"result": "succeeded",
"resourceChanges": {
"update": 3,
"delete": 1,
"update-replace": 2
},
"isPreview": false
}
Stack Preview
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"projectName": "website",
"stackName": "website-prod",
"updateUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/previews/11bf162b-d9d5-4715-8f88-20dcd0e0b167",
"kind": "update",
"result": "failed",
"resourceChanges": {
"update": 3,
"delete": 1,
"update-replace": 2
},
"isPreview": true
}
Deployment
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"projectName": "website",
"stackName": "website-prod",
"deploymentUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/deployments/127",
"version": 127,
"operation": "update",
"status": "running"
}
Drift detection
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"projectName": "website",
"stackName": "website-prod",
"driftDetected": true,
"driftRunId": "11bf162b-d9d5-4715-8f88-20dcd0e0b167",
"status": "succeeded",
"resourceChanges": { "update": 3, "delete": 1 },
"referenceUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/deployments/127"
}
Drift remediation
{
"user": {
"name": "Morty Smith",
"githubLogin": "morty",
"avatarUrl": "https://crazy-adventures.net/morty.png"
},
"organization": {
"name": "Crazy Adventures",
"githubLogin": "crazy-adventures",
"avatarUrl": "https://crazy-adventures.net/logo.png"
},
"projectName": "website",
"stackName": "website-prod",
"status": "succeeded",
"resourceChanges": { "update": 3, "delete": 1 },
"referenceUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/deployments/128"
}
Headers
Payloads contain several headers.
Header | Description |
---|---|
Pulumi-Webhook-ID | Unique ID for each webhook sent which you can reference when looking at delivery logs in the Pulumi Cloud. |
Pulumi-Webhook-Kind | The kind of webhook event, e.g. stack_update . |
Pulumi-Webhook-Signature | Only set if the webhook has a shared secret. HMAC hex digest of the request payload, using the sha256 hash function and the webhook secret as the HMAC key. |
The following snippets show how to compute and verify the webhook signature. For examples in other languages, see danharper/hmac-examples.
var crypto = require('crypto');
const sharedSecret = ...
const payload = req.body.toString();
var hmacAlg = crypto.createHmac('sha256', sharedSecret);
var expectedSignature = hmac.update(payloadBody).digest('hex');
import * as crypto from "crypto";
const sharedSecret = ...
const payload = req.body.toString();
const hmacAlg = crypto.createHmac("sha256", stackConfig.sharedSecret);
const hmac = hmacAlg.update(payload).digest("hex");
const result = crypto.timingSafeEqual(Buffer.from(webhookSig), Buffer.from(hmac));
import hashlib
import hmac
import base64
message = bytes('...', 'utf-8')
secret = bytes('...', 'utf-8')
hash = hmac.new(secret, message, hashlib.sha256)
hash.hexdigest()
expected_signature = base64.b64encode(hash.digest())
func computeSignature(payload []byte, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write(payload)
contract.AssertNoErrorf(err, "computing HMAC digest")
return fmt.Sprintf("%x", mac.Sum(nil))
}
Additional Resources
Thank you for your feedback!
If you have a question about how to use Pulumi, reach out in Community Slack.
Open an issue on GitHub to report a problem or suggest an improvement.