Webhook icon
Schedule icon
WorkingDirectory icon
Clone icon
Commands icon
Docker icon
Script icon
Process icon
If icon
Create icon
SlackIncomingWebhook icon
HumanTask icon
Log icon
Fail icon

Pulumi Preview Policy Gate with Human Approval

pulumi preview gated by forbidden types, delete rules, and blast-radius limits - violations open a GitHub issue and wait for approval before pulumi up.

Categories
Infrastructure

How it works

  1. ci_webhook (event-based Webhook) receives the post-merge call from CI, or nightly_recheck (shipped disabled) re-gates a standing stack.
  2. workspace (core.flow.WorkingDirectory) opens one shared filesystem: clone_repository (git.Clone) checks out the IaC repository, and run_preview (scripts.shell.Commands with a Docker taskRunner on pulumi/pulumi:latest, shape from terraform-infracost-budget-gate) selects the stack and writes pulumi preview --json to preview.json.
  3. evaluate_preview (scripts.python.Script, Process runner) reads the plan and applies three ratchet rules: no forbidden_types may be created, updated, or replaced (exact or prefix match on the resource type parsed from each entry's type/urn, tolerant of both singular op-key spellings and before/after wrappers); delete/replace operations are violations unless allow_deletes is true; and more than max_changes changing resources trips the blast-radius limit. A missing or empty preview.json raises immediately - a broken gate never looks like a cheap change. Results return through Kestra's ::json:: outputs protocol as violations, change_counts, and a report_markdown table (read back via the script vars namespace).
  4. violation_gate (core.flow.If) branches on violation_count: over zero, open_issue (github.issues.Create) files a labeled pulumi/policy-gate issue carrying the report, notify_violations posts it to Slack, and human_approval (io.kestra.plugin.ee.flow.HumanTask) assigns the decision to the platform-approvers group with behavior: FAIL and a structured onResume (approve/deny plus reviewer note). approval_gate checks (outputs.human_approval.onResume is defined) and ... == 'approve' (the proven condition shape from iac-opa-policy-gate-human-approval) and fails the run on denial or timeout. Clean plans take the else branch and just log.
  5. Only executions that passed the gate reach apply_workspace - a second WorkingDirectory that re-clones the repository so preview and apply never share scratch state - where apply_stack runs pulumi up --yes in the same container image, and announce_applied reports the resource count and the review path taken.
  6. Outputs expose stack, violation_count, violations, change_counts, report_markdown, and human_decision (with an is defined fallback to not-required); errors alerts Slack with wording that a failed preview must never be read as a passing gate.

What you get

  • Plan-as-policy: risky resource types, destructive operations, and oversized changes are caught before pulumi up, not discovered after.
  • Violations become GitHub issues with the full report, so exceptions are worked like any other backlog item.
  • Durable human approval for violating plans, attributable to a group with a stored rationale; clean plans never wait on anyone.
  • One execution history containing the plan, the decision, and the apply.

Who it's for

Platform teams running Pulumi who want a policy ratchet between merge and apply, and regulated environments that must show who approved an infrastructure exception and why.

Why orchestrate this with Kestra

A CI step can fail a plan, but it cannot hold a durable approval for two days, file the backlog ticket, alert the right channel, and resume exactly where it left off - and it certainly cannot prove afterwards which human waved the exception through. Kestra keeps the pause, the issue, the Slack thread, and the apply in one replayable execution.

Prerequisites

  • A Pulumi project in the target repository and a logged-in backend (token and state passphrase configurable via secrets).
  • Docker available to the worker for the Pulumi container and a GitHub token that can clone the repository and open issues.
  • A Slack incoming webhook and a platform-approvers group in Kestra.
  • Tuned forbidden_types, allow_deletes, and max_changes inputs for your environment.

Secrets

  • GITHUB_USERNAME: Git username for cloning the IaC repository.
  • GITHUB_TOKEN: GitHub token with repository read and issue-create rights.
  • PULUMI_ACCESS_TOKEN: Pulumi access token for the backend and org services.
  • PULUMI_CONFIG_PASSPHRASE: Passphrase decrypting stack configuration secrets.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for gate notifications.

Quick start

  1. Set the five secrets and create the platform-approvers group.
  2. Point repository_url/github_repository at the IaC repo and stack at the stack to gate.
  3. Run with default inputs on a plan that creates an aws:iam:Role: the issue files, Slack posts the report, and the HumanTask waits.
  4. Resume with approve + a note and watch pulumi up run; resume with deny and the run fails with nothing applied.
  5. Wire ci_webhook into the pipeline so every merge is gated the same way.

How to extend

  • Add rules to evaluate - forbid public exposure (0.0.0.0/0 ingress), require tags, or ratchet monthly cost via Infracost alongside the plan.
  • Replace the single gate with severity tiers: auto-apply warnings, require approval only for hard denials, mirroring the OPA blueprint.
  • Post the issue URL back to the PR with github.pulls.Create or a comment task once you confirm the issue output field in your plugin version.
  • Keep a plan history in S3 to trend blast radius per stack over time.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.