Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 9 min read

Slack Notifications for Terraform Cloud Runs: Plans, Applies, and Approvals

Terraform Cloud can fire run events at Slack, but the default payloads are thin. Here's how to turn plan and apply events into reviewable, actionable messages.

  • #slack
  • #terraform
  • #infrastructure-as-code
  • #iac
  • #devops
  • #automation

Terraform Cloud (and Terraform Enterprise) will happily ping a Slack webhook every time a run changes state. Most teams turn that on, get buried in “run started / run planned / run applied” noise, and turn it back off. That’s a waste — the run lifecycle is exactly the kind of thing that belongs in Slack, because infra changes deserve eyes before they apply. The fix isn’t to disable notifications; it’s to make them carry the information that makes them worth reading.

Here’s how I wire Terraform Cloud into Slack so plans get reviewed and applies don’t surprise anyone.

The two events that matter

Terraform Cloud fires notifications across the whole run lifecycle, but for Slack you really care about two transitions:

  1. Planned and awaiting confirmation — a plan finished and wants an apply. This is the one a human needs to look at.
  2. Applied / errored — the run finished. This is the audit trail.

Everything else (run:created, run:planning) is noise in a chat channel. The native generic webhook lets you subscribe to specific triggers, so subscribe narrowly.

The native notification is a starting point, not the answer

Terraform Cloud’s built-in Slack notification posts a basic message with a link to the run. It’s fine for “something happened,” but it doesn’t tell you what will change — and “what will change” is the entire point of reviewing a plan. The native payload won’t say “this destroys the production database.” A link does, but only if someone clicks it, and people don’t click.

So I use the generic webhook notification type instead and point it at a small relay — a tiny service or serverless function — that enriches the event before posting to Slack.

The relay pattern

The flow is: Terraform Cloud → your relay → Slack. The relay receives the run event, calls the Terraform Cloud API to pull the plan’s resource-change summary, and renders a Block Kit message.

// relay: receives TFC generic webhook, enriches, posts to Slack
app.post('/tfc-webhook', async (req, res) => {
  verifyTfcSignature(req);               // HMAC over the body
  res.sendStatus(200);                   // ack fast

  const { run_id, workspace_name, trigger } = parse(req.body);
  if (trigger !== 'run:needs_attention') return;

  // Pull the plan summary from the TFC API
  const plan = await tfc.getPlan(run_id);
  const { add, change, destroy } = plan.resource_changes_summary;

  await slack.chat.postMessage({
    channel: '#infra-changes',
    blocks: renderRunBlocks({ workspace_name, run_id, add, change, destroy }),
  });
});

The add / change / destroy counts are the headline. A plan that destroys 0 resources reads completely differently from one that destroys 14, and the message should make that obvious at a glance.

Make destroys impossible to miss

The single most useful thing you can do is visually escalate destructive plans. Zero destroys: green, low-key. Any destroys: red header, @-mention the platform on-call, and put the count in the title.

{
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "⚠️ prod-network: plan destroys 3 resources" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*Add:*\n+5" },
        { "type": "mrkdwn", "text": "*Change:*\n~2" },
        { "type": "mrkdwn", "text": "*Destroy:*\n-3" },
        { "type": "mrkdwn", "text": "*Workspace:*\n`prod-network`" }
      ]
    },
    {
      "type": "actions",
      "elements": [
        { "type": "button", "text": { "type": "plain_text", "text": "🔍 Review plan" }, "url": "https://app.terraform.io/app/org/workspaces/prod-network/runs/run-abc" }
      ]
    }
  ]
}

You can list the actual resources slated for destruction in the message body too — pulled from the plan’s JSON — so reviewers see which resources without opening the run. For a network or database workspace, that’s the difference between catching a mistake and apologizing for one.

Approvals from Slack — carefully

Terraform Cloud’s API lets you confirm or discard a run, which means you can put an “Apply” button in the Slack message. Whether you should depends on your risk tolerance. My rule: apply-from-Slack is fine for low-blast-radius workspaces (a dev environment, a DNS record), and it should be off for anything that can take down production. When I do enable it, the button hits a relay endpoint that checks the clicker’s identity against an allowlist before calling the TFC confirm API, and it always posts who approved.

app.action('tfc_apply', async ({ ack, body, client }) => {
  await ack();
  if (!isApprover(body.user.id)) {
    return client.chat.postEphemeral({ channel: body.channel.id, user: body.user.id,
      text: '🚫 You are not on the apply allowlist for this workspace.' });
  }
  await tfc.applyRun(extractRunId(body));
  await client.chat.postMessage({ channel: body.channel.id,
    text: `✅ <@${body.user.id}> applied the run.` });
});

Close the loop on apply

When the run finishes, update the original message (capture the ts) or post a threaded reply: applied successfully, or errored with the error class. Don’t leave a “needs attention” message sitting there forever after it’s been applied — a stale approval prompt is how someone double-applies.

Security notes

Terraform Cloud signs its generic webhooks with an HMAC; verify it on every inbound call or your relay is an open door to your apply path. Keep your TFC API token scoped to the minimum (read plans, confirm runs) and out of the repo. And never echo full plan output into a public channel — Terraform plans can leak resource names, IPs, and occasionally values you’d rather not broadcast.

Where to start

Subscribe to just the two events that matter, stand up the relay, and get the add/change/destroy counts into the message. That alone makes plans reviewable. Add Slack approvals only for low-risk workspaces, and only behind an allowlist. For more on designing actionable ops messages, see our Slack for ops guides.

Applying infrastructure changes from chat modifies real systems. Verify plans, restrict approvals, and confirm results before trusting automated apply flows.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.