Subscribes a delivery endpoint to events raised at a given scope — an Event Grid topic, domain, or system topic, or a resource group / subscription — with optional filtering, dead-lettering, retry, and identity-based delivery. Targets
hashicorp/azurerm ~> 4.0.
This module creates and manages a single Event Grid event subscription and everything that hangs off it:
- 🎯 A subscription at any scope — an Event Grid topic, domain, or system topic, or a resource group / subscription for platform events.
- 📮 Exactly one delivery endpoint — Event Hub, Service Bus queue or topic, Storage Queue, Azure Function, webhook, or Hybrid Connection.
- 🔎 Optional subject and advanced (attribute-based) filtering across 19 operators.
- 🧾 Optional retry policy, dead-lettering to a Storage blob container, and identity-based delivery / dead-lettering.
- 🔐 Optional delivery properties (headers) whose values may carry a secret — wrapped so they never surface in plan output.
💡 Why it matters: an event subscription is the wire between a producer and a consumer. Getting its filter, retry, and dead-letter behavior right — and keeping a secret delivery header out of state diffs — is the difference between a reliable event pipeline and silently dropped events or a leaked credential.
If this module saves you time, a little support goes a long way:
- ⭐ Star the repository on GitHub.
- 🔗 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
scope["terraform-azurerm-eventgrid-topic / -domain / -system-topic"]
me["terraform-azurerm-eventgrid-event-subscription"]
v["azurerm_eventgrid_event_subscription"]
tgt["delivery target (Event Hub / Service Bus / Storage Queue / Function / webhook)"]
dl["dead-letter Storage account"]
scope -->|"scope"| me
me -->|"creates"| v
v -->|"delivers to"| tgt
v -.->|"dead-letters to"| dl
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class me me;
class v target;
class scope,tgt,dl ext;
The scope module (topic / domain / system topic) feeds this module a scope; a delivery-target module feeds an endpoint ID; an optional Storage account receives dead-lettered events.
flowchart LR
in_scope["scope / event_delivery_schema"]
in_ep["exactly one endpoint (eventhub / service_bus / storage_queue / webhook / azure_function / hybrid)"]
in_filter["subject_filter / advanced_filter / included_event_types"]
in_rel["retry_policy / dead_letter / delivery_identity / delivery_properties"]
res["azurerm_eventgrid_event_subscription.this"]
out_id["id / name"]
in_scope -->|"input"| res
in_ep -->|"input"| res
in_filter -->|"input"| res
in_rel -->|"input (secrets wrapped)"| res
res -->|"output"| out_id
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory (block set on the keystone azurerm_eventgrid_event_subscription.this):
| Block / field | Cardinality | Role |
|---|---|---|
name, scope, event_delivery_schema |
scalar | Identity + schema (all force-new). |
eventhub_endpoint_id / hybrid_connection_endpoint_id / service_bus_queue_endpoint_id / service_bus_topic_endpoint_id |
scalar | Delivery endpoint (one of seven). |
azure_function_endpoint / storage_queue_endpoint / webhook_endpoint |
dynamic (0–1) | Delivery endpoint (one of seven). |
subject_filter |
dynamic (0–1) | Subject prefix/suffix filter. |
advanced_filter |
dynamic (0–1) | 19 operator sub-blocks (bool / number / string). |
included_event_types, labels, advanced_filtering_on_arrays_enabled, expiration_time_utc |
scalar | Filtering / metadata. |
retry_policy |
dynamic (0–1) | Max attempts + TTL. |
storage_blob_dead_letter_destination |
dynamic (0–1) | Dead-letter target. |
delivery_identity / dead_letter_identity |
dynamic (0–1 each) | Managed identity for delivery / dead-lettering. |
delivery_property |
dynamic (0–N) | Headers; secret values wrapped sensitive(). |
timeouts |
dynamic (0–1) | Per-operation timeouts. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm", including the mandatory features {} block, plus auth. |
Schema notes that bite:
- 🔒
name,scope, andevent_delivery_schemaare force-new — changing any of them replaces the subscription. - 🎯 Exactly one delivery endpoint must be supplied across the seven endpoint inputs; the module enforces this at parse time with a
validation {}block. - 🪝
dead_letter_identityrequiresstorage_blob_dead_letter_destinationto also be set, or Azure rejects the apply. - 🔐 A
delivery_propertyvalue is treated as sensitive — it is wrappedsensitive()inmain.tfand never appears in plan output. - 🔎
advanced_filterexposes 19 operators (bool_equals,is_not_null,is_null_or_undefined, sixnumber_*scalars,number_in/number_not_in/number_in_range/number_not_in_range, and eightstring_*list operators).
Least-privilege, at the source scope plus the delivery target:
Microsoft.EventGrid/eventSubscriptions/write,/read,/deleteat the scope (the built-in EventGrid Contributor covers this at the target scope).- For identity-based delivery, the delivery identity needs the data-plane send role on the target, for example:
- Azure Event Hubs Data Sender
- Azure Service Bus Data Sender
- Storage Queue Data Message Sender
- For identity-based dead-lettering, the dead-letter identity needs blob write access on the destination container (Storage Blob Data Contributor).
- An existing source scope (Event Grid topic / domain / system topic, or a resource group / subscription) and an existing delivery target.
- The
Microsoft.EventGridresource provider registered on the subscription. - For dead-lettering with an identity, a Storage account and the identity's blob write role on the destination container.
terraform-azurerm-eventgrid-event-subscription/
├── providers.tf # required_version + azurerm ~> 4.0 pin; no provider block
├── variables.tf # deeply-typed object() schemas; exactly-one-endpoint + enum validations; timeouts tail
├── main.tf # keystone azurerm_eventgrid_event_subscription.this; dynamic endpoint/filter blocks; sensitive() delivery_property values
├── outputs.tf # id (first), name
├── README.md # this file
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
The smallest real call — a scope and exactly one endpoint:
provider "azurerm" {
features {}
}
module "order_events" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "order-events-to-hub"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
}ℹ️ The caller configures the provider, authentication, and the mandatory
features {}block. This module never declares aproviderblock.
Consumes:
| Input | Type | Source module |
|---|---|---|
scope |
string |
terraform-azurerm-eventgrid-topic / -domain / -system-topic |
eventhub_endpoint_id |
string |
terraform-azurerm-eventhub-namespace (an event hub) |
service_bus_queue_endpoint_id / service_bus_topic_endpoint_id |
string |
terraform-azurerm-servicebus-namespace |
storage_queue_endpoint.storage_account_id / storage_blob_dead_letter_destination.storage_account_id |
string |
terraform-azurerm-storage-account |
delivery_identity.user_assigned_identity / dead_letter_identity.user_assigned_identity |
string |
terraform-azurerm-user-assigned-identity |
Emits:
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the event subscription | references |
name |
Subscription name | references |
1 · Webhook endpoint (Entra-protected)
module "webhook_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-webhook"
scope = azurerm_eventgrid_topic.orders.id
webhook_endpoint = {
url = "https://api.contoso.com/eventgrid/orders"
active_directory_app_id_or_uri = "api://contoso-eventgrid"
active_directory_tenant_id = var.tenant_id
max_events_per_batch = 10
}
}🔒 Prefer an Entra-protected webhook (
active_directory_*) over an unauthenticated URL so Event Grid presents a bearer token the endpoint can validate.
2 · Event Hub endpoint
module "hub_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-hub"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
}💡 The endpoint is a specific event hub ID, not the namespace ID.
3 · Service Bus queue endpoint
module "sbq_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-sbq"
scope = azurerm_eventgrid_topic.orders.id
service_bus_queue_endpoint_id = azurerm_servicebus_queue.orders.id
}4 · Service Bus topic endpoint
module "sbt_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-sbt"
scope = azurerm_eventgrid_topic.orders.id
service_bus_topic_endpoint_id = azurerm_servicebus_topic.orders.id
}
⚠️ Supply exactly one endpoint. Passing both a queue and a topic ID fails the parse-timevalidation {}block.
5 · Storage Queue endpoint
module "queue_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-queue"
scope = azurerm_eventgrid_topic.orders.id
storage_queue_endpoint = {
storage_account_id = azurerm_storage_account.events.id
queue_name = "orders"
queue_message_time_to_live_in_seconds = 604800
}
}6 · Azure Function endpoint
module "fn_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-function"
scope = azurerm_eventgrid_topic.orders.id
azure_function_endpoint = {
function_id = "${azurerm_linux_function_app.processor.id}/functions/HandleOrder"
max_events_per_batch = 1
preferred_batch_size_in_kilobytes = 64
}
}ℹ️
function_idis the fully-qualified function resource ID (the app ID plus/functions/<name>).
7 · Subject filter
module "subject_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-prefix-filtered"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
subject_filter = {
subject_begins_with = "/orders/us/"
subject_ends_with = ".json"
case_sensitive = false
}
}💡 Subject filters are cheap and evaluated first — use them before reaching for advanced filters.
8 · Advanced filter — string_in
module "color_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-red-blue"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
advanced_filter = {
string_in = [{ key = "data.color", values = ["red", "blue"] }]
}
}9 · Advanced filter — number_greater_than + bool_equals
module "priority_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "high-value-priority-orders"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
advanced_filter = {
number_greater_than = [{ key = "data.amount", value = 1000 }]
bool_equals = [{ key = "data.priority", value = true }]
}
advanced_filtering_on_arrays_enabled = true
}
⚠️ A type mismatch (e.g.number_greater_thanagainst a string field) fails at delivery evaluation, not at plan — keep the operator aligned with the attribute type.
10 · Dead-letter + dead-letter identity
module "dlq_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-with-deadletter"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
storage_blob_dead_letter_destination = {
storage_account_id = azurerm_storage_account.events.id
storage_blob_container_name = "deadletter"
}
dead_letter_identity = {
type = "SystemAssigned"
}
}
⚠️ dead_letter_identityrequiresstorage_blob_dead_letter_destination— set both or neither.
11 · Retry policy
module "retry_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-tight-retry"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
retry_policy = {
max_delivery_attempts = 10
event_time_to_live = 60
}
}💡
event_time_to_liveis in minutes; events older than this are dropped (or dead-lettered, if configured).
12 · Delivery properties with a secret header
module "header_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-authenticated-webhook"
scope = azurerm_eventgrid_topic.orders.id
webhook_endpoint = {
url = "https://api.contoso.com/eventgrid/orders"
}
delivery_properties = [
{
header_name = "X-Environment"
type = "Static"
value = "production"
},
{
header_name = "Authorization"
type = "Static"
value = var.webhook_bearer_token # provision out of band; never commit
secret = true
},
]
}🔒 Mark credential-bearing headers
secret = true. The module wraps the valuesensitive()so it never appears in plan output. Pass the value as a reference from a variable or a Key Vault data source — do not commit it.
13 · Identity-based delivery to Event Hub
module "identity_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-identity-delivery"
scope = azurerm_eventgrid_topic.orders.id
eventhub_endpoint_id = azurerm_eventhub.orders.id
delivery_identity = {
type = "UserAssigned"
user_assigned_identity = azurerm_user_assigned_identity.eventgrid.id
}
}🔒 Prefer a managed identity with the Azure Event Hubs Data Sender role over an embedded credential. Grant the role before applying, or delivery fails silently.
14 · CloudEvent schema + included event types
module "cloudevent_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-cloudevents"
scope = azurerm_eventgrid_topic.orders.id
event_delivery_schema = "CloudEventSchemaV1_0"
eventhub_endpoint_id = azurerm_eventhub.orders.id
included_event_types = ["Contoso.Orders.Created", "Contoso.Orders.Cancelled"]
labels = ["team-fulfilment", "prod"]
}
⚠️ event_delivery_schemais force-new — switching schema after creation replaces the subscription.
15 · 🏗️ End-to-end composition
Wire a resource group, an Event Grid topic, an Event Hubs namespace (delivery), a Storage account (dead-letter), and a user-assigned identity into a single subscription:
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-events-prod"
location = "eastus2"
}
module "topic" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-topic.git?ref=v1.0.0"
name = "orders-topic"
resource_group_name = module.rg.name
location = module.rg.location
}
module "hub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventhub-namespace.git?ref=v1.0.0"
name = "orders-ns"
resource_group_name = module.rg.name
location = module.rg.location
event_hubs = {
orders = { name = "orders", partition_count = 4, message_retention = 7 }
}
}
module "deadletter" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "steventsdlq01"
resource_group_name = module.rg.name
location = module.rg.location
containers = {
deadletter = {}
}
}
module "eventgrid_identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
name = "id-eventgrid-delivery"
resource_group_name = module.rg.name
location = module.rg.location
}
module "subscription" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
name = "orders-to-hub"
scope = module.topic.id
event_delivery_schema = "CloudEventSchemaV1_0"
eventhub_endpoint_id = module.hub.eventhub_ids["orders"]
delivery_identity = {
type = "UserAssigned"
user_assigned_identity = module.eventgrid_identity.id
}
subject_filter = {
subject_begins_with = "/orders/us/"
}
advanced_filter = {
number_greater_than = [{ key = "data.amount", value = 500 }]
}
storage_blob_dead_letter_destination = {
storage_account_id = module.deadletter.id
storage_blob_container_name = "deadletter"
}
dead_letter_identity = {
type = "UserAssigned"
user_assigned_identity = module.eventgrid_identity.id
}
retry_policy = {
max_delivery_attempts = 20
event_time_to_live = 120
}
}💡 The delivery identity must hold Azure Event Hubs Data Sender on the hub and Storage Blob Data Contributor on the dead-letter container before this applies cleanly. Grant those role assignments in the same composition.
Identity & schema: name (required), scope (required), event_delivery_schema, expiration_time_utc, included_event_types, labels, advanced_filtering_on_arrays_enabled.
Delivery endpoint (supply exactly one): eventhub_endpoint_id, hybrid_connection_endpoint_id, service_bus_queue_endpoint_id, service_bus_topic_endpoint_id, azure_function_endpoint, storage_queue_endpoint, webhook_endpoint.
Filtering: subject_filter, advanced_filter.
Reliability & identity: retry_policy, storage_blob_dead_letter_destination, delivery_identity, dead_letter_identity, delivery_properties.
Universal tail: timeouts (no tags — this resource does not support them).
Full input schemas
variable "name" { type = string } # force-new
variable "scope" { type = string } # force-new
variable "event_delivery_schema" { # force-new; null = EventGridSchema
type = string # EventGridSchema | CloudEventSchemaV1_0 | CustomInputSchema
default = null
}
variable "expiration_time_utc" { type = string; default = null }
variable "included_event_types" { type = list(string); default = null }
variable "labels" { type = list(string); default = null }
variable "advanced_filtering_on_arrays_enabled" { type = bool; default = null }
# --- Delivery endpoint — supply exactly one -------------------------------
variable "eventhub_endpoint_id" { type = string; default = null }
variable "hybrid_connection_endpoint_id" { type = string; default = null }
variable "service_bus_queue_endpoint_id" { type = string; default = null }
variable "service_bus_topic_endpoint_id" { type = string; default = null }
variable "azure_function_endpoint" {
type = object({
function_id = string
max_events_per_batch = optional(number)
preferred_batch_size_in_kilobytes = optional(number)
})
default = null
}
variable "storage_queue_endpoint" {
type = object({
storage_account_id = string
queue_name = string
queue_message_time_to_live_in_seconds = optional(number)
})
default = null
}
variable "webhook_endpoint" {
type = object({
url = string
active_directory_app_id_or_uri = optional(string)
active_directory_tenant_id = optional(string)
max_events_per_batch = optional(number)
preferred_batch_size_in_kilobytes = optional(number)
})
default = null
}
# --- Filtering ------------------------------------------------------------
variable "subject_filter" {
type = object({
subject_begins_with = optional(string)
subject_ends_with = optional(string)
case_sensitive = optional(bool)
})
default = null
}
variable "advanced_filter" {
type = object({
bool_equals = optional(list(object({ key = string, value = bool })))
is_not_null = optional(list(object({ key = string })))
is_null_or_undefined = optional(list(object({ key = string })))
number_greater_than = optional(list(object({ key = string, value = number })))
number_greater_than_or_equals = optional(list(object({ key = string, value = number })))
number_less_than = optional(list(object({ key = string, value = number })))
number_less_than_or_equals = optional(list(object({ key = string, value = number })))
number_in = optional(list(object({ key = string, values = list(number) })))
number_not_in = optional(list(object({ key = string, values = list(number) })))
number_in_range = optional(list(object({ key = string, values = list(list(number)) })))
number_not_in_range = optional(list(object({ key = string, values = list(list(number)) })))
string_begins_with = optional(list(object({ key = string, values = list(string) })))
string_not_begins_with = optional(list(object({ key = string, values = list(string) })))
string_ends_with = optional(list(object({ key = string, values = list(string) })))
string_not_ends_with = optional(list(object({ key = string, values = list(string) })))
string_contains = optional(list(object({ key = string, values = list(string) })))
string_not_contains = optional(list(object({ key = string, values = list(string) })))
string_in = optional(list(object({ key = string, values = list(string) })))
string_not_in = optional(list(object({ key = string, values = list(string) })))
})
default = null
}
# --- Reliability & identity ----------------------------------------------
variable "delivery_identity" { # type = SystemAssigned | UserAssigned
type = object({
type = string
user_assigned_identity = optional(string)
})
default = null
}
variable "dead_letter_identity" { # requires storage_blob_dead_letter_destination
type = object({
type = string # SystemAssigned | UserAssigned
user_assigned_identity = optional(string)
})
default = null
}
variable "storage_blob_dead_letter_destination" {
type = object({
storage_account_id = string
storage_blob_container_name = string
})
default = null
}
variable "retry_policy" {
type = object({
max_delivery_attempts = number
event_time_to_live = number
})
default = null
}
variable "delivery_properties" { # value is SENSITIVE (wrapped in main.tf)
type = list(object({
header_name = string
type = string
source_field = optional(string)
value = optional(string)
secret = optional(bool)
}))
default = []
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Kind |
|---|---|---|
id |
Passthrough | |
name |
Name of the event subscription | Passthrough |
scope |
Resource ID of the topic, domain, system topic, resource group or subscription this subscription listens to | Passthrough |
event_delivery_schema |
Envelope events are delivered in: "EventGridSchema" (the default), "CloudEventSchemaV1_0" or "CustomInputSchema" | Passthrough |
delivery_endpoint_kind |
Passthrough | |
delivery_target_id |
Passthrough | |
eventhub_endpoint_id |
Event hub the subscription delivers to, or null | Passthrough |
hybrid_connection_endpoint_id |
Relay hybrid connection the subscription delivers to, or null | Passthrough |
service_bus_queue_endpoint_id |
Service Bus queue the subscription delivers to, or null | Passthrough |
service_bus_topic_endpoint_id |
Service Bus topic the subscription delivers to, or null | Passthrough |
storage_queue_name |
Name of the Storage queue events are delivered into, or null when the destination is not a Storage queue | Derived |
webhook_base_url |
Derived | |
has_webhook_endpoint |
Whether the destination is a webhook | Derived |
webhook_full_url_is_stored_unredacted_in_state |
Derived | |
refresh_requires_get_full_url_permission |
Constant | |
webhook_entra_configuration_is_incomplete |
Passthrough | |
webhook_is_entra_protected |
Whether the webhook is configured to receive an Entra ID bearer token, stated positively: both Entra fields are present | Passthrough |
webhook_creation_requires_a_live_handler |
Derived | |
included_event_types |
Event types this subscription accepts, read back from Azure | Passthrough |
advanced_filter_value_count |
Derived | |
advanced_filter_values_remaining |
How many of the 25 advanced-filter values are still unspent | Derived |
advanced_filter_keys |
Every event attribute the advanced filter tests, de-duplicated | Derived |
subscription_filters_nothing |
Passthrough | |
subject_filter_never_converges |
Passthrough | |
array_filtering_is_disabled |
Passthrough | |
max_delivery_attempts |
Delivery attempts before Event Grid gives up on an event, read back from Azure | Derived |
event_time_to_live_minutes |
How long, in minutes, Event Grid keeps trying before abandoning an event, read back from Azure | Derived |
uses_default_retry_policy |
Whether the retry behaviour is Azure's default rather than one this configuration chose | Derived |
dead_letter_enabled |
Whether undeliverable events are written to a Storage blob container instead of being discarded | Derived |
dead_letter_container_id |
Resource ID of the Storage account receiving dead-lettered events, or null when dead-lettering is not configured | Derived |
undeliverable_events_are_discarded |
Derived | |
dead_lettering_is_best_effort |
Derived | |
delivery_identity_type |
Managed identity type Event Grid uses to deliver events ("SystemAssigned" or "UserAssigned"), or null when delivery runs as Event Grid's own service principal | Derived |
dead_letter_identity_type |
Managed identity type used for writing dead-lettered events, or null when Event Grid's own service principal writes them | Derived |
delivery_uses_managed_identity |
Whether delivery authenticates as a managed identity rather than relying on Event Grid's service principal or on a credential baked into the endpoint | Derived |
identity_holds_no_permission_from_this_module |
Derived | |
delivery_property_names |
Header names attached to each delivered event, in configuration order | Derived |
has_secret_delivery_property |
Whether any delivery header is flagged secret | Derived |
delivery_properties_are_discarded |
Derived | |
secret_delivery_property_values_are_in_terraform_state |
Derived | |
module_emits_no_delivery_property_value |
Constant | |
expiration_time_utc |
Moment after which Azure deletes the subscription, or an empty string when it never expires | Passthrough |
expires_and_is_then_deleted |
Derived | |
supports_no_tags |
Constant | |
creates_no_role_assignment |
Always true | Constant |
replacement_loses_events_in_flight |
Constant |
This module emits no secrets. Secret delivery_property values are consumed as inputs and never re-emitted.
- Force-new fields.
name,scope, andevent_delivery_schemaare immutable. Terraform replaces the subscription when any of them changes; plan for a brief gap in delivery when that happens. - Exactly-one-endpoint rule. The seven endpoint inputs are mutually exclusive. A
validation {}block onwebhook_endpointcounts the non-null endpoints and rejects the plan unless exactly one is set — the error is raised at parse time, before any Azure call. - Dead-letter identity dependency.
dead_letter_identityis only meaningful alongsidestorage_blob_dead_letter_destination; Azure rejects an identity without a destination. Set both together. - Sensitive delivery properties. In
main.tfeachdelivery_property.valueis wrappedsensitive(), so a secret header (bearer token, API key) never appears in plan output or logs. Always mark such a propertysecret = trueand pass its value by reference. features {}dependence. Theazurermprovider will not initialize without a caller-sideprovider "azurerm" { features {} }block. This module deliberately omits anyproviderblock; if init fails in isolation, that missing caller block is the usual cause.for_eachover dynamic blocks. Every optional endpoint / filter / identity block is rendered via a presence-guardeddynamicblock withtry(...)on optional fields, so an omitted input renders nothing rather than a null-bearing block.
| Principle | How this module applies it |
|---|---|
| Deeply-typed nested schemas | Every endpoint, filter, identity, retry, and dead-letter block is a typed object() — a typo in a nested key is a parse-time type error, not a runtime surprise. |
| Parse-time validation | Exactly-one-endpoint is enforced with a validation {} block; event_delivery_schema, delivery_identity.type, and dead_letter_identity.type are enum-validated with actionable messages. |
Secrets wrapped sensitive() |
delivery_property values are wrapped so secret headers never render in plan output; mark them secret = true and pass by reference. |
| Identity-preferred delivery | delivery_identity / dead_letter_identity are exposed so a managed identity can carry delivery, avoiding embedded credentials. |
| No tags | The resource does not support tags; the universal tail is timeouts only. |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module by tag — ?ref=v1.0.0 — never a branch. This library is plan-only; a human applies from CI after review. Never run terraform apply from an authoring workstation.
The offline proof gate covers structure and typing without touching Azure:
terraform init -backend=false— resolves the provider without a backend.terraform validate— proves the schema, the deeply-typedobject()inputs, the exactly-one-endpointvalidation {}block, and every enum validation.terraform fmt -check— proves canonical formatting.
What only terraform plan (against a real subscription and provider auth) exercises: whether the scope and endpoint IDs resolve, whether the delivery identity holds the required data-plane role, and whether Azure accepts the filter/retry/dead-letter combination. Those are integration concerns a human runs from CI.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-events-prod/providers/Microsoft.EventGrid/topics/orders-topic/providers/Microsoft.EventGrid/eventSubscriptions/orders-to-hub"
name = "orders-to-hub"
A secret delivery_property value is never printed — it renders as <sensitive> in plan and apply output.
| Symptom | Cause | Fix |
|---|---|---|
Supply exactly one delivery endpoint at plan |
Zero or more than one endpoint input set. | Provide exactly one of the seven endpoint inputs; remove the extras. |
| Apply rejected — dead-letter identity without destination | dead_letter_identity set but storage_blob_dead_letter_destination omitted. |
Set storage_blob_dead_letter_destination too, or remove dead_letter_identity. |
| Events not matching an advanced filter | Operator type does not match the attribute type (e.g. number_greater_than on a string). |
Align the operator with the attribute's JSON type; check advanced_filtering_on_arrays_enabled for array-valued keys. |
event_delivery_schema change forces replacement |
event_delivery_schema is force-new. |
Expected — accept the replacement, or create a second subscription and cut over. |
| Delivery fails silently with an identity | The delivery identity lacks the target's data-plane send role. | Grant the identity the appropriate Data Sender role at the target before applying. |
| Provider fails to initialize in isolation | Caller has no provider "azurerm" { features {} } block. |
Add the features {} block in the root module — the module never carries one. |
- Provider resource:
azurerm_eventgrid_event_subscription - Scope siblings:
terraform-azurerm-eventgrid-topic,terraform-azurerm-eventgrid-domain,terraform-azurerm-eventgrid-system-topic - Where the parent is a system topic, prefer
terraform-azurerm-eventgrid-system-topic-event-subscription: it addresses the topic by name plus resource group rather than by ascopeResource ID, so a parent reference is not interchangeable between the two modules - Delivery-target siblings:
terraform-azurerm-eventhub-namespace,terraform-azurerm-servicebus-namespace,terraform-azurerm-storage-account,terraform-azurerm-user-assigned-identity - This module's cross-module contract:
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."

