Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Automation Webhook Terraform Module

A callable HTTPS endpoint that triggers an Azure Automation runbook when invoked. Targets hashicorp/azurerm ~> 4.0, plan-only, secure by default.


Terraform azurerm Module Type Resources Posture


🧩 Overview

This module manages a single Azure Automation Webhook β€” a secret-bearing HTTPS URL that starts a runbook when an external system POSTs to it.

  • ☁️ Creates one azurerm_automation_webhook keystone resource.
  • πŸ”— References an existing Automation account and a published runbook by name β€” it does not create either.
  • πŸ”’ Treats the trigger uri as a secret on both the input and the output side (both sensitive = true).
  • 🎲 Prefers a provider-generated, unguessable URI over a caller-supplied one.
  • ⏱️ Requires a future expiry_time, after which the webhook stops working.
  • πŸ–₯️ Optionally routes the triggered job to a hybrid runbook worker group.
  • 🧾 Passes an optional lowercase-keyed parameters map to the runbook on each trigger.

πŸ’‘ Why it matters: A webhook lets systems outside Azure launch a runbook without holding an Azure credential. The trade-off is that the trigger URI is the credential β€” anyone with it can run the runbook until it expires. This module keeps that URI sensitive end to end so it never lands in plan output or state diffs a reviewer can read.


❀️ Support this project

If this module saves you time, please consider supporting it:


πŸ—ΊοΈ Where this fits in the family

graph TD
  me["terraform-azurerm-automation-webhook"]
  target["azurerm_automation_webhook"]
  acct["azurerm_automation_account"]
  rb["azurerm_automation_runbook"]
  caller["external caller"]
  me -->|"creates"| target
  acct -->|"contains"| target
  rb -->|"triggered by"| target
  caller -->|"POSTs to uri"| target
  classDef me fill:#0078D4,color:#ffffff,stroke:#004578;
  classDef target fill:#004578,color:#ffffff,stroke:#002a44;
  classDef ext fill:#f2f2f2,color:#000000,stroke:#cccccc;
  class me me;
  class target target;
  class acct,rb,caller ext;
Loading

The webhook lives inside an Automation account and points at a runbook that account contains. Both are owned by sibling modules and consumed here by name. The external caller holds the trigger URI.


🧬 What this module builds

graph LR
  in_name["name / runbook_name"]
  in_expiry["expiry_time / enabled"]
  in_params["parameters / run_on_worker_group"]
  res["azurerm_automation_webhook.this"]
  out_id["id"]
  out_name["name"]
  out_uri["uri (sensitive)"]
  in_name -->|"input"| res
  in_expiry -->|"input"| res
  in_params -->|"input"| res
  res -->|"output"| out_id
  res -->|"output"| out_name
  res -->|"secret output"| out_uri
  classDef me fill:#0078D4,color:#ffffff,stroke:#004578;
  class res me;
Loading

Resource inventory

Resource Cardinality Role
azurerm_automation_webhook.this single (keystone) The webhook endpoint that triggers the runbook.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures provider "azurerm", its auth, and the mandatory features {} block.

Schema notes that bite

  • 🧱 name, resource_group_name, and automation_account_name are force-new β€” changing any of them destroys and recreates the webhook (and regenerates its URI).
  • πŸ”’ uri is a secret returned only at creation. Azure does not expose it on subsequent reads, so if you lose it you must recreate the webhook to get a new one.
  • ⏱️ expiry_time is required and must be a future RFC 3339 timestamp; an expired webhook cannot be invoked.
  • πŸ”‘ parameters keys must be lowercase β€” an Azure Automation API requirement, enforced by a validation block in this module.
  • 🏷️ This resource does not support tags, so the universal tail is timeouts only.

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege at the Automation account scope:

  • Microsoft.Automation/automationAccounts/webhooks/write
  • Microsoft.Automation/automationAccounts/webhooks/read
  • Microsoft.Automation/automationAccounts/webhooks/delete
  • Microsoft.Automation/automationAccounts/webhooks/action (to generate the URI)

The built-in Automation Contributor role covers these; scope it to the Automation account or its resource group rather than the whole subscription.


Azure Prerequisites

  • An existing Automation account and a published runbook to target.
  • The Microsoft.Automation resource provider registered on the subscription.
  • The caller's provider "azurerm" block configured with valid auth and a features {} block.

πŸ“ Module Structure

terraform-azurerm-automation-webhook/
β”œβ”€β”€ providers.tf     # required_version + azurerm ~> 4.0 pin; no provider block
β”œβ”€β”€ variables.tf     # deeply-typed inputs; sensitive uri; timeouts tail (no tags)
β”œβ”€β”€ main.tf          # the azurerm_automation_webhook.this keystone + dynamic timeouts
β”œβ”€β”€ outputs.tf       # id, name, and the sensitive uri
β”œβ”€β”€ README.md        # this document
β”œβ”€β”€ SCOPE.md         # the cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore       # canonical library ignore set

βš™οΈ Quick Start

The smallest real call β€” let Azure generate the URI, then read it back out of band:

module "start_backup_webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "start-nightly-backup"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Invoke-NightlyBackup"
  expiry_time             = "2027-01-01T00:00:00Z"
}

ℹ️ The caller configures provider "azurerm" { features {} }, its subscription, and its authentication. This module declares no provider block and accepts no auth inputs.

πŸ”’ Do not supply a uri. Leaving it null lets Azure mint a secure, unguessable one, exposed once via the sensitive uri output.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group
automation_account_name string terraform-azurerm-automation-account
runbook_name string terraform-azurerm-automation-runbook

Emits

Output Description Sensitive
id Resource ID of the webhook (emitted first). no
name Webhook name. no
uri The secret trigger URI β€” anyone holding it can start the runbook. yes

πŸ“š Example Library

1 Β· Minimal β€” provider-generated URI
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-report"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Generate-Report"
  expiry_time             = "2027-06-30T00:00:00Z"
}

πŸ”’ Preferred pattern: omit uri so Azure generates one. Read it once from the sensitive uri output.

2 Β· Disabled webhook (staged, not yet live)
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-failover"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Invoke-Failover"
  expiry_time             = "2027-12-31T00:00:00Z"
  enabled                 = false
}

ℹ️ enabled = false creates the webhook and its URI but rejects invocations until you flip it to true.

3 Β· Passing runbook parameters
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-scale"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Scale-Cluster"
  expiry_time             = "2027-03-01T00:00:00Z"

  parameters = {
    environment = "production"
    replicas    = "6"
  }
}

⚠️ Keys must be lowercase β€” Environment would fail the module's validation. Never place secrets in parameters; reference an Automation credential or variable asset from inside the runbook instead.

4 Β· Hybrid runbook worker group
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-onprem-patch"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Patch-OnPremFleet"
  expiry_time             = "2027-09-01T00:00:00Z"
  run_on_worker_group     = "onprem-datacenter-1"
}

ℹ️ Leave run_on_worker_group null (the default) to run the job on Azure sandbox infrastructure.

5 Β· Short-lived webhook (near-term expiry)
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "one-off-migration"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Run-Migration"
  expiry_time             = "2026-08-01T00:00:00Z"
}

⚠️ A near-term expiry_time narrows the window an exposed URI can be abused. Set it to just past your planned cutover, not years out.

6 Β· Supplying your own URI (secret, discouraged)
variable "webhook_uri" {
  type      = string
  sensitive = true
}

module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-legacy"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Invoke-Legacy"
  expiry_time             = "2027-02-01T00:00:00Z"
  uri                     = var.webhook_uri
}

πŸ”’ Only supply a uri when an existing integration already depends on a specific value. Provision it out of band (a secure pipeline variable or key vault reference) β€” never commit it. The provider-generated URI in example 1 is the safer default.

7 Β· Reading the URI into a downstream secret store
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-ingest"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Start-Ingest"
  expiry_time             = "2027-04-01T00:00:00Z"
}

resource "azurerm_key_vault_secret" "webhook_uri" {
  name         = "ingest-webhook-uri"
  value        = module.webhook.uri_is_not_emitted_by_design # the URI is never emitted; read it once from this module state and store it
  key_vault_id = var.secrets_vault_id
}

πŸ”’ Because the URI is returned only at creation, capture it immediately into a secret store the invoking system can read. The value flows through as sensitive.

8 Β· Multiple webhooks with for_each
locals {
  webhooks = {
    backup  = "Invoke-NightlyBackup"
    report  = "Generate-Report"
    cleanup = "Purge-StaleData"
  }
}

module "webhook" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"
  for_each = local.webhooks

  name                    = "trigger-${each.key}"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = each.value
  expiry_time             = "2027-01-01T00:00:00Z"
}

πŸ’‘ Keying by a stable map key keeps each webhook independent β€” adding or removing one never re-creates the others.

9 Β· Custom timeouts
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-slow-account"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Invoke-Task"
  expiry_time             = "2027-05-01T00:00:00Z"

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}

ℹ️ Provide only the operations you want to override; omitted fields fall back to provider defaults.

10 Β· Parameters plus a worker group together
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "trigger-hybrid-deploy"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Deploy-Release"
  expiry_time             = "2027-07-15T00:00:00Z"
  run_on_worker_group     = "build-agents"

  parameters = {
    release = "2027.07.1"
    channel = "stable"
  }
}

⚠️ Parameters are passed on every trigger. The invoking caller cannot override them per call for this resource type, so bake per-environment values into distinct webhooks.

11 Β· Non-production webhook with tight lifecycle
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "dev-trigger-smoketest"
  resource_group_name     = "rg-automation-dev"
  automation_account_name = "aa-platform-dev"
  runbook_name            = "Run-SmokeTest"
  expiry_time             = "2026-09-01T00:00:00Z"
  enabled                 = true
}

πŸ’‘ In non-production, pair a short expiry with a disabled default until the integration is wired, then enable it.

12 Β· Output wiring for a CI pipeline variable
module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "ci-trigger-deploy"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-platform-prod"
  runbook_name            = "Invoke-Deploy"
  expiry_time             = "2027-01-01T00:00:00Z"
}

output "deploy_webhook_uri" {
  value     = module.webhook.uri_is_not_emitted_by_design # the URI is never emitted; read it once from this module state and store it
  sensitive = true
}

output "deploy_webhook_id" {
  value = module.webhook.id
}

πŸ”’ Re-declaring the URI as a sensitive root output keeps it masked in CI logs. The id is safe to print.

13 Β· πŸ—οΈ End-to-end composition (account + runbook β†’ webhook)
module "resource_group" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-automation-prod"
  location = "eastus"
}

module "automation_account" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
  name                = "aa-platform-prod"
  resource_group_name = module.resource_group.name
  location            = "eastus"
}

module "runbook" {
  source                  = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-runbook.git?ref=v1.0.0"

  location              = "eastus2"

  runbook_type          = "PowerShell"
  name                    = "Invoke-NightlyBackup"
  resource_group_name     = module.resource_group.name
  automation_account_name = module.automation_account.name
}

module "webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-webhook.git?ref=v1.0.0"

  name                    = "start-nightly-backup"
  resource_group_name     = module.resource_group.name
  automation_account_name = module.automation_account.name
  runbook_name            = module.runbook.name
  expiry_time             = "2027-01-01T00:00:00Z"
}

resource "azurerm_key_vault_secret" "backup_webhook_uri" {
  name         = "nightly-backup-webhook-uri"
  value        = module.webhook.uri_is_not_emitted_by_design # the URI is never emitted; read it once from this module state and store it
  key_vault_id = var.secrets_vault_id
}

πŸ’‘ The resource group, Automation account, and runbook feed their names straight into the webhook. The secret URI lands in Key Vault the moment it is created, ready for the external scheduler to read.


πŸ“₯ Inputs

Required

Name Type Description
name string Webhook name. Force-new.
resource_group_name string Resource group holding the Automation account. Force-new.
automation_account_name string Parent Automation account. Force-new.
runbook_name string Runbook this webhook triggers.
expiry_time string Future RFC 3339 expiry timestamp.

Optional

Name Type Default Description
enabled bool true Whether the webhook accepts invocations.
run_on_worker_group string null Hybrid worker group; null runs on Azure.
parameters map(string) null Lowercase-keyed parameters passed to the runbook.
uri string (sensitive) null Optional caller-supplied URI; null lets Azure generate one.
timeouts object(...) null Per-operation timeouts.
Full object() schemas
variable "name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "automation_account_name" {
  type = string
}

variable "runbook_name" {
  type = string
}

variable "expiry_time" {
  type = string
}

variable "enabled" {
  type    = bool
  default = true
}

variable "run_on_worker_group" {
  type    = string
  default = null
}

variable "parameters" {
  type    = map(string)
  default = null
  # keys must be lowercase (Azure Automation API requirement)
}

variable "uri" {
  type      = string
  default   = null
  sensitive = true
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Kind
id Resource ID of the Automation webhook Passthrough
name Name of the webhook, as created Passthrough
resource_group_name Resource group holding the parent Automation account Passthrough
automation_account_name Name of the parent Automation account that owns this webhook Passthrough
runbook_name Name of the runbook this webhook starts, as Azure returned it Passthrough
expiry_time When the webhook stops working, as Azure returned it Passthrough
enabled Whether the webhook accepts calls, as Azure returned it Passthrough
run_on_worker_group Name of the Hybrid Runbook Worker group the triggered job runs on, as Azure returned it Passthrough
runs_on_hybrid_worker_group Whether the triggered job runs on a Hybrid Runbook Worker group rather than in the Azure sandbox Derived
parameter_names Sorted list of the parameter names passed to the runbook on every call Derived
parameter_count Number of parameters passed to the runbook on every call Derived
uri_is_not_emitted_by_design Constant true, and the most important thing to know about this module's contract Constant
uri_is_returned_by_azure_only_at_creation Constant true Constant
uri_cannot_be_recovered_only_reissued Constant true, and the reason a lost URI is expensive rather than annoying Constant
uri_is_a_bearer_credential Constant true Constant
uri_is_stored_in_state_in_plaintext Constant true Constant
uri_is_generated_by_azure Whether the URI was generated by Azure rather than supplied by the caller Passthrough
changing_expiry_time_replaces_the_webhook_and_its_uri Constant true, and the divergence between the platform and the provider that costs the most Constant
changing_runbook_name_produces_a_permanent_diff Constant true, and a defect in the pinned provider version rather than a design note Constant
disabling_is_the_way_to_revoke_without_losing_the_uri Constant true, and the useful counterpart to everything above Constant
webhook_is_armed_on_creation Whether this webhook accepts calls from the moment it is created Derived
an_expired_webhook_still_exists_and_plans_clean Constant true Constant
expiry_time_is_compared_as_an_instant Constant true Constant
parameters_are_visible_in_plan_state_and_job_history Constant true, and the reason this module does not mark the parameters map sensitive Constant
parameters_are_replaced_whole_not_merged Constant true Constant
fields_azure_returns_on_read The fields the provider's read populates from the Azure API, and therefore the only fields in which Terraform can detect drift Derived
invocation_requires_no_azure_identity Constant true, and the property that makes a webhook useful and dangerous in the same breath Constant

🧠 Architecture Notes

  • Force-new identity fields. name, resource_group_name, and automation_account_name are immutable. Terraform cannot rename a webhook in place; a change to any of these plans a destroy-and-recreate, which also produces a brand-new URI and invalidates the old one.
  • The URI is a one-time secret. Azure returns the trigger URI only when the webhook is created. It is never re-exposed on later reads, so the module surfaces it through a sensitive = true output and never renders it in plan text. If the value is lost, the only recovery is to recreate the webhook. The optional uri input is likewise sensitive = true; prefer to leave it null and let Azure mint an unguessable one.
  • Expiry is a hard stop. expiry_time must be a future RFC 3339 timestamp. Once passed, the webhook rejects invocations even while the resource still exists β€” this is a common cause of a runbook silently no longer firing. Rotate by recreating the webhook with a new expiry (and a new URI).
  • Parameters are fixed and lowercase. The parameters map is sent on every trigger; keys must be lowercase, enforced by a validation block. This is a place secrets must never go β€” reference Automation credential or variable assets from inside the runbook instead.
  • No tags. This resource type does not support tags, so the universal tail carries timeouts only.
  • features {} dependence. Like every module in this suite, this module declares no provider block. If it appears not to initialize in isolation, the caller is almost certainly missing the required provider "azurerm" { features {} } block in the root module.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Trigger URI generation uri omitted β†’ Azure generates a secure, unguessable URI supply your own uri (sensitive, out of band)
URI input handling uri input is sensitive = true β€”
URI output handling uri output is sensitive = true, returned once β€”
Secret in parameters not accepted β€” reference Automation assets from the runbook β€”
Lifetime expiry_time required, must be a future timestamp β€”
  • Prefer the provider-generated URI; treat a caller-supplied one as the exception.
  • Provision secrets out of band. The trigger URI is a credential β€” capture it into a secret store, never a committed file or a plain output.

πŸš€ Runbook

This module is plan-only. A human applies from CI.

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the source at a tag β€” ?ref=v1.0.0 β€” never a branch.
  • No cloud apply happens in authoring or review; everything is static analysis until a human runs terraform apply from CI.

πŸ§ͺ Testing

The offline proof gate for this module:

  • terraform init -backend=false β€” resolves the provider without a backend.
  • terraform validate β€” proves the type contract holds: the parameters-lowercase validation, the object shapes, and every reference resolve.
  • terraform fmt -check β€” proves canonical formatting.

What only a live terraform plan against a real subscription exercises: that the referenced Automation account and runbook exist, that RBAC permits webhook creation, and that Azure accepts the expiry_time. Those run in CI against a real provider, not during authoring.


πŸ’¬ Example Output

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

id   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-automation-prod/providers/Microsoft.Automation/automationAccounts/aa-platform-prod/webhooks/start-nightly-backup"
name = "start-nightly-backup"
uri  = <sensitive>

πŸ”’ The uri renders as <sensitive>. Read it out of band with terraform output -raw uri only into a secure destination β€” never into a shared log.


πŸ” Troubleshooting

Symptom Cause Fix
Webhook returns HTTP 400/410 when called expiry_time has passed β€” an expired webhook cannot be invoked. Recreate the webhook with a future expiry_time; distribute the new URI.
Lost the trigger URI Azure returns the URI only at creation; it is never re-exposed. Recreate the webhook to mint a new URI, then capture it into a secret store immediately.
parameters keys must be lowercase at plan A parameters key uses uppercase. Lowercase every key (for example Environment β†’ environment).
Plan shows destroy/recreate after a rename name, resource_group_name, or automation_account_name changed β€” all force-new. Expect a new resource and a new URI; re-wire consumers of the URI.
Runbook never fires despite POSTs Webhook created with enabled = false. Set enabled = true.
Provider fails to initialize in isolation Caller root module is missing provider "azurerm" { features {} }. Add the features {} block to the caller's provider configuration.

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."