A callable HTTPS endpoint that triggers an Azure Automation runbook when invoked. Targets
hashicorp/azurerm ~> 4.0, plan-only, secure by default.
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_webhookkeystone resource. - π References an existing Automation account and a published runbook by name β it does not create either.
- π Treats the trigger
urias a secret on both the input and the output side (bothsensitive = 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
parametersmap 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.
If this module saves you time, please consider supporting it:
- β Star the repository on GitHub.
- πΌ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_automation_webhook.this |
single (keystone) | The webhook endpoint that triggers the runbook. |
| 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, andautomation_account_nameare force-new β changing any of them destroys and recreates the webhook (and regenerates its URI). - π
uriis 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_timeis required and must be a future RFC 3339 timestamp; an expired webhook cannot be invoked. - π‘
parameterskeys 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 istimeoutsonly.
Least-privilege at the Automation account scope:
Microsoft.Automation/automationAccounts/webhooks/writeMicrosoft.Automation/automationAccounts/webhooks/readMicrosoft.Automation/automationAccounts/webhooks/deleteMicrosoft.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.
- An existing Automation account and a published runbook to target.
- The
Microsoft.Automationresource provider registered on the subscription. - The caller's
provider "azurerm"block configured with valid auth and afeatures {}block.
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
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 sensitiveurioutput.
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 |
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
uriso Azure generates one. Read it once from the sensitiveurioutput.
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 = falsecreates the webhook and its URI but rejects invocations until you flip it totrue.
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 βEnvironmentwould fail the module's validation. Never place secrets inparameters; 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_groupnull (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-termexpiry_timenarrows 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
uriwhen 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
idis 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.
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
}| 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 |
- Force-new identity fields.
name,resource_group_name, andautomation_account_nameare 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 = trueoutput and never renders it in plan text. If the value is lost, the only recovery is to recreate the webhook. The optionaluriinput is likewisesensitive = true; prefer to leave it null and let Azure mint an unguessable one. - Expiry is a hard stop.
expiry_timemust 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
parametersmap 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
timeoutsonly. 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 requiredprovider "azurerm" { features {} }block in the root module.
| 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.
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 applyfrom CI.
The offline proof gate for this module:
terraform init -backend=falseβ resolves the provider without a backend.terraform validateβ proves the type contract holds: theparameters-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.
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
urirenders as<sensitive>. Read it out of band withterraform output -raw urionly into a secure destination β never into a shared log.
| 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. |
- Provider resource:
azurerm_automation_webhook - Provider resource:
azurerm_automation_account - Provider resource:
azurerm_automation_runbook - Microsoft Learn: Starting a runbook from a webhook
- Sibling modules:
terraform-azurerm-automation-account,terraform-azurerm-automation-runbook,terraform-azurerm-resource-group - This module's
SCOPE.mdβ the cross-module contract.
π "Infrastructure as Code should be standardized, consistent, and secure."