Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Eventgrid System Topic Terraform Module

A standalone module that provisions a single azurerm_eventgrid_system_topic — the Event Grid projection of an Azure resource's system events — targeting hashicorp/azurerm ~> 4.0. The empty call ships with a system-assigned managed identity so downstream delivery runs under role-based access control, not shared keys.

Terraform azurerm Module Type Resources


🧩 Overview

  • 📡 Provisions one Event Grid system topic — the managed representation of the platform events a source Azure resource (a Storage account, a resource group, a subscription, a Key Vault, and many more) already emits.
  • 🔗 Binds to a source via source_arm_resource_id and topic_type, the two immutable fields that identify which resource's events flow through the topic.
  • 🪪 Identity-first delivery — a system-assigned managed identity by default, so event subscriptions built on this topic can deliver to endpoints under role-based access control with no shared keys to distribute.
  • 🧾 Metric-ready — emits metric_arm_resource_id so a sibling diagnostic-settings or metric-alert module can target the topic directly.
  • 🧱 Composable, single-purpose — the topic only; event subscriptions, diagnostics, private connectivity, and role grants are deliberately left to sibling modules and wired by id.

💡 Why it matters: A system topic is the seam between an Azure resource's built-in event stream and the subscriptions that route those events to handlers. Shipping it with a managed identity by default means the very first subscription you attach can deliver events using Entra ID and least-privilege role assignments instead of a long-lived key — the safe posture is the default, and removing the identity is the deliberate step.


❤️ Support this project

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


🗺️ Where this fits in the family

flowchart LR
  RG["terraform-azurerm-resource-group"]
  SRC["Source resource (Storage / RG / Subscription)"]
  ID["terraform-azurerm-user-assigned-identity"]
  RA["terraform-azurerm-role-assignments"]
  ST["terraform-azurerm-eventgrid-system-topic"]
  SUB["terraform-azurerm-eventgrid-event-subscription"]
  HANDLER["Handlers (Functions / Queues / Webhooks)"]

  RG -->|"resource_group_name + location"| ST
  SRC -->|"source_arm_resource_id + topic_type"| ST
  ID -->|"identity_ids"| ST
  ST -->|"identity principal delivers under RBAC"| RA
  ST -->|"system topic id"| SUB
  SUB -->|"delivers events"| HANDLER

  classDef self fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#002b4d,color:#ffffff;
  classDef sibling fill:#eef2f7,stroke:#c7d2e0,color:#1b2a3a;

  class ST self;
  class SUB keystone;
  class RG,SRC,ID,RA,HANDLER sibling;
Loading

This module owns only the system topic. It consumes a resource group, a source resource, and (optionally) a user-assigned identity by reference, and it is consumed downstream by event subscriptions, role assignments, and diagnostic settings — each by the topic's id (or metric_arm_resource_id for metrics).


🧬 What this module builds

flowchart LR
  subgraph INPUTS["Inputs"]
    N["name / resource_group_name / location"]
    SRC["source_arm_resource_id / topic_type"]
    IDN["identity (default SystemAssigned)"]
    T["tags / timeouts"]
  end

  ST["azurerm_eventgrid_system_topic.this"]

  subgraph OUTPUTS["Outputs"]
    OID["id"]
    ONAME["name"]
    OMET["metric_arm_resource_id"]
    OPRIN["identity_principal_id"]
  end

  N -->|"required identity"| ST
  SRC -->|"event source binding"| ST
  IDN -->|"dynamic identity block"| ST
  T -->|"tags + timeouts"| ST

  ST -->|"emits"| OID
  ST -->|"emits"| ONAME
  ST -->|"emits"| OMET
  ST -->|"emits"| OPRIN

  classDef self fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#002b4d,color:#ffffff;
  classDef sibling fill:#eef2f7,stroke:#c7d2e0,color:#1b2a3a;

  class ST keystone;
  class N,SRC,IDN,T,OID,ONAME,OMET,OPRIN sibling;
Loading

Resource inventory

Resource Count Role
azurerm_eventgrid_system_topic.this 1 The keystone system topic, including its optional identity and timeouts blocks.

✅ Provider / Versions

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

Schema notes that bite (verified against the live provider schema):

  • 🔁 name, resource_group_name, and location are force-new — changing any of them replaces the system topic.
  • 🔁 source_arm_resource_id and topic_type are force-new — a system topic is permanently bound to one source resource of one kind. Repointing it at a different source, or changing its type, forces replacement.
  • 🧭 topic_type must match the kind of resource named by source_arm_resource_id (for example Microsoft.Storage.StorageAccounts with a Storage account ID). The set of topic types is defined by the platform and grows over time, so it is accepted as a free-form string rather than a closed enum.
  • 🪪 identity and tags are the only fields that update in place; everything else is immutable.
  • 📊 id, metric_arm_resource_id, and the identity's principal_id / tenant_id are computed — known only after apply.
  • 🧱 A system topic has no public-network, local-auth, or firewall knobs of its own. It is a passive projection of a source's events; those controls live on the source resource and on the event subscriptions attached to this topic.

🔑 Required Azure RBAC Roles / Permissions

  • Create / manage the system topic: EventGrid Contributor on the target resource group (or Contributor), or a custom role with Microsoft.EventGrid/systemTopics/* at resource-group scope.
  • Bind to the source resource: read access (for example Reader) on the resource named by source_arm_resource_id, so Event Grid can attach the topic to that source.
  • Event delivery (data plane): grant the system topic's identity principal the role each delivery target requires (for example Storage Queue Data Message Sender on a queue, or EventGrid Data Sender on another topic). Do this with a sibling role-assignments module at the target's scope, using identity_principal_id from the outputs.

Grant the least privilege at the smallest scope that works.


Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.EventGrid resource provider registered on the target subscription.
  • The source resource already provisioned — its Resource ID is source_arm_resource_id, and its own resource provider (for example Microsoft.Storage) registered.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription — this module declares none of them.

📁 Module Structure

terraform-azurerm-eventgrid-system-topic/
├── providers.tf     # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf     # deeply-typed object() schemas + tags/timeouts tail
├── main.tf          # azurerm_eventgrid_system_topic.this; dynamic identity / timeouts blocks
├── outputs.tf       # id first, then name, metric_arm_resource_id, identity principal/tenant
├── README.md        # this document
├── SCOPE.md         # the cross-module contract
├── LICENSE          # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore       # the canonical library ignore set

⚙️ Quick Start

The smallest real call — a system topic for a Storage account, with the default managed identity:

provider "azurerm" {
  features {}
}

module "eventgrid_system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-orders-storage"
  resource_group_name    = "rg-eventing-prod"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-prod/providers/Microsoft.Storage/storageAccounts/stordersprod"

  tags = {
    environment = "prod"
    workload    = "orders"
  }
}

ℹ️ The caller owns the provider, authentication, and the mandatory features {} block. Pin the module with ?ref=v1.0.0 — never a branch.


🔌 Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group (name)
location string caller / terraform-azurerm-resource-group (location)
source_arm_resource_id string the source module's id (e.g. terraform-azurerm-storage-account)
identity.identity_ids set(string) terraform-azurerm-user-assigned-identity (id)

Emits

Output Description Consumed by
id System topic Resource ID (first) eventgrid-event-subscription, role assignments, diagnostics
name System topic name diagnostics / tagging
metric_arm_resource_id Resource ID for platform metrics metric alerts, diagnostic settings
identity_principal_id System-assigned identity principal ID (null when none) role assignments on delivery targets
identity_tenant_id Managed identity tenant ID (null when none) documentation / delivery configuration

📚 Example Library

1 · Minimal system topic for a Storage account
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-orders-storage"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id
}

🔒 The empty call already carries a system-assigned managed identity, so the first subscription you attach can deliver events under role-based access control without a shared key.

2 · System topic for a resource group
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-rg-audit"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Resources.ResourceGroups"
  source_arm_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-workload-prod"
}

💡 A resource-group system topic surfaces Microsoft.Resources.ResourceWriteSuccess/Delete and similar control-plane events for everything in that group.

3 · Subscription-level system topic
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-subscription-audit"
  resource_group_name    = "rg-governance"
  location               = "eastus2"
  topic_type             = "Microsoft.Resources.Subscriptions"
  source_arm_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000"
}

ℹ️ For subscription-scoped events, source_arm_resource_id is the bare subscription ID and topic_type is Microsoft.Resources.Subscriptions. The topic still lives inside a resource group you nominate.

4 · System topic for a Key Vault
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-kv-secrets"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.KeyVault.vaults"
  source_arm_resource_id = var.key_vault_id
}

💡 Key Vault system topics emit secret/key/certificate near-expiry and new-version events — a common trigger for rotation automation.

5 · Explicit system-assigned identity
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-sysid"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id

  identity = {
    type = "SystemAssigned"
  }
}

ℹ️ This is the module default written out. Use identity_principal_id from the outputs to grant the topic access to identity-authenticated delivery targets.

6 · User-assigned identity for delivery
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-uami"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id

  identity = {
    type         = "UserAssigned"
    identity_ids = [var.user_assigned_identity_id]
  }
}

ℹ️ identity_ids is required whenever type includes UserAssigned; the module validates this at parse time.

7 · Both identity types together
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-mixed-id"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id

  identity = {
    type         = "SystemAssigned, UserAssigned"
    identity_ids = [var.user_assigned_identity_id]
  }
}

ℹ️ The combined value must be written exactly as "SystemAssigned, UserAssigned" (with the comma and space).

8 · No managed identity (opt out)
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-no-identity"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id

  identity = null
}

⚠️ Opting out of the identity means any subscription on this topic must deliver with an endpoint that does not require the topic's identity (for example a storage-queue destination secured a different way). Prefer leaving the default system-assigned identity in place.

9 · Fully tagged, timeouts tuned
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-governed"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = var.storage_account_id

  tags = {
    environment = "prod"
    owner       = "platform-eventing"
    cost_center = "cc-4471"
  }

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

💡 Tags update in place; apply your organization's tagging convention here without risking replacement.

10 · for_each across many source accounts
locals {
  sources = {
    orders    = var.orders_storage_account_id
    inventory = var.inventory_storage_account_id
    shipping  = var.shipping_storage_account_id
  }
}

module "system_topic" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"
  for_each = local.sources

  name                   = "egst-${each.key}"
  resource_group_name    = "rg-eventing"
  location               = "eastus2"
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = each.value

  tags = { workload = each.key }
}

💡 Fan the module out with for_each to stamp one system topic per source account with a consistent shape.

11 · Grant the topic identity a delivery role (sibling wiring)
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-delivery"
  resource_group_name    = module.rg.name
  location               = module.rg.location
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = module.storage.id
}

# Sibling module grants the topic's identity permission to deliver to a queue.
module "delivery_access" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.storage.id
  role_assignments = {
    queue_delivery = {
      role_definition_name = "Storage Queue Data Message Sender"
      principal_id         = module.system_topic.identity_principal_id
    }
  }
}

🔒 Delivery runs under the topic's managed identity and a least-privilege role — no key is created or shared.

12 · Diagnostic settings by metric id (sibling wiring)
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-monitored"
  resource_group_name    = module.rg.name
  location               = module.rg.location
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = module.storage.id
}

# Sibling diagnostic-settings module targets the topic's resource id.
module "system_topic_diagnostics" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"

  name                       = "diag-egst"
  target_resource_id         = module.system_topic.id
  log_analytics_workspace_id = var.log_analytics_workspace_id
}

ℹ️ Use id for a diagnostic setting; use metric_arm_resource_id when a metric alert needs the metrics resource specifically.

13 · Event subscription on the topic (sibling wiring)
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-with-sub"
  resource_group_name    = module.rg.name
  location               = module.rg.location
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = module.storage.id
}

# The event subscription is a separate module keyed off the system topic name.
# module "subscription" {
#   source           = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-event-subscription.git?ref=v1.0.0"
#   system_topic     = module.system_topic.name
#   ...
# }

ℹ️ Event subscriptions are intentionally a sibling module. A system-topic subscription references this topic by name (within the same resource group) rather than owning it here.

14 · Least-privilege delivery to a second topic
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-fanout"
  resource_group_name    = module.rg.name
  location               = module.rg.location
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = module.storage.id

  identity = {
    type = "SystemAssigned"
  }
}

# Grant only EventGrid Data Sender on the downstream topic — nothing broader.
module "fanout_access" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = var.downstream_topic_id
  role_assignments = {
    fanout = {
      role_definition_name = "EventGrid Data Sender"
      principal_id         = module.system_topic.identity_principal_id
    }
  }
}

🔒 Scope the role assignment to the single downstream topic, not the resource group, so the identity can send only where it must.

15 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

# 1) Resource group
module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-eventing-prod"
  location = "eastus2"
  tags     = { environment = "prod", workload = "orders" }
}

# 2) The source resource whose events we want — a Storage account
module "storage" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"

  name                = "stordersprod"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

# 3) The Event Grid system topic — bound to the Storage account, identity by default
module "system_topic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-eventgrid-system-topic.git?ref=v1.0.0"

  name                   = "egst-orders-storage"
  resource_group_name    = module.rg.name
  location               = module.rg.location
  topic_type             = "Microsoft.Storage.StorageAccounts"
  source_arm_resource_id = module.storage.id

  tags = { environment = "prod" }
}

# 4) Grant the topic identity permission to deliver into a storage queue, least-privilege
module "delivery_access" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.storage.id
  role_assignments = {
    queue_delivery = {
      role_definition_name = "Storage Queue Data Message Sender"
      principal_id         = module.system_topic.identity_principal_id
    }
  }
}

# 5) An event subscription (sibling module) consumes the system topic downstream.
#    module "subscription" { source = ".../terraform-azurerm-eventgrid-event-subscription..."
#      system_topic = module.system_topic.name  ... }

output "system_topic_id" {
  value = module.system_topic.id
}

🔒 The topic delivers under its managed identity and a single least-privilege role assignment scoped to the source — no shared key is created or distributed. The event subscription is intentionally a sibling module, wired by module.system_topic.name.


📥 Inputs

Required

Name Type Description
name string System topic name (force-new).
resource_group_name string Existing resource group (force-new).
location string Azure region (force-new).
topic_type string Source resource kind, e.g. Microsoft.Storage.StorageAccounts (force-new).
source_arm_resource_id string Resource ID of the source resource (force-new).

Optional — identity & tail

Name Type Default Description
identity object { type = "SystemAssigned" } Managed identity block; type enum-validated; identity_ids required for UserAssigned. Set null for no identity.
tags map(string) {} Resource tags (mutable).
timeouts object null Create/read/update/delete timeouts.
Full object() schemas
variable "identity" {
  type = object({
    type         = string                 # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
    identity_ids = optional(set(string))  # required when type includes UserAssigned
  })
  default = {
    type = "SystemAssigned"
  }
}

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

🧾 Outputs

Output Description Kind
id Passthrough
name Passthrough
resource_group_name Resource group containing the system topic Passthrough
location Azure region of the system topic, normalised by the provider (for example "eastus") Passthrough
tags Tags applied to the system topic, echoed so a composition can stamp sibling resources with the same set Passthrough
topic_type System topic type, for example "Microsoft.Storage.StorageAccounts" Passthrough
source_resource_id Passthrough
source_arm_resource_id Passthrough
source_scope_kind Passthrough
source_subscription_id Subscription ID parsed out of the source Resource ID, or null for a management-group source Derived
source_is_global_scope_but_location_is_regional Passthrough
topic_type_is_not_checked_against_the_source Constant
source_binding_replacement_destroys_subscriptions Constant
is_singleton_per_event_source Constant
metric_resource_id Passthrough
metric_arm_resource_id The same metrics identifier under the deprecated provider attribute name, which is removed in provider 5.0 Passthrough
identity_type Managed identity type configured on the system topic ("SystemAssigned", "UserAssigned" or "SystemAssigned, UserAssigned"), or null when the topic has no managed identity Derived
identity_principal_id Derived
identity_tenant_id Tenant ID of the system topic's managed identity, or null when the topic has no managed identity Derived
identity_ids Set of user-assigned identity Resource IDs attached to the system topic Derived
has_managed_identity Whether the system topic carries any managed identity Derived
has_system_assigned_identity Whether a system-assigned identity is present, stated separately because only this kind populates identity_principal_id Derived
has_user_assigned_identity Whether at least one user-assigned identity is attached Derived
identity_grants_nothing_by_itself Derived
read_does_not_list_access_keys Constant
emits_no_secret Constant
has_no_network_or_local_auth_controls Constant
uses_deprecated_source_argument Constant
creates_no_event_subscription Constant
creates_no_role_assignment Always true Constant
event_subscription_capacity Passthrough

🔒 No secret is emitted. Delivery is expected to run under the managed identity and role assignments granted on downstream targets.


🧠 Architecture Notes

  • Single keystone. The module owns exactly one resource, azurerm_eventgrid_system_topic.this. The optional nested blocks (identity, timeouts) are rendered with dynamic blocks guarded by try(...), so an omitted input renders as absent rather than an error.
  • Secure default is an identity, not a lock. Unlike a data-plane service, a system topic has no public-access or firewall surface to close. Its meaningful secure default is identity-based delivery: the empty call attaches a system-assigned identity so downstream subscriptions authenticate with Entra ID and role assignments. Removing it (identity = null) is the explicit opt-out.
  • The source binding is permanent. source_arm_resource_id and topic_type are force-new and must agree — the ID must reference a resource of the kind named by the type. Repointing at a new source, or switching type, replaces the topic and any subscriptions built on it.
  • topic_type is an open string. The platform adds new system-topic types over time, so this module accepts the value as a validated-shape string rather than a closed enum that could reject a legal, newer type.
  • Metrics vs resource id. Diagnostic settings and role assignments target id; metric alerts that need the metrics resource specifically use metric_arm_resource_id. Both are emitted.
  • features {} dependence. The module carries no provider {} block. If it appears not to initialize in isolation, the cause is a missing caller-side provider "azurerm" { features {} } block — that is expected.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Delivery credential system-assigned managed identity (identity = { type = "SystemAssigned" }) set identity = null, or supply a user-assigned identity
Embedded secrets none accepted or emitted — (not supported by design)
Identity type safety type validated against the legal set; identity_ids required for UserAssigned —
Source binding explicit source_arm_resource_id + topic_type, both immutable — (a new source means a new topic)
  • The delivery credential defaults to a managed identity so no key is ever distributed.
  • The identity.type enum is enforced with a validation {} block listing the legal values.
  • The module never accepts or emits a plaintext secret.

🚀 Runbook

# From the module folder (offline, plan-only):
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • No terraform apply runs during authoring; a human applies from CI against real credentials.
  • The caller supplies the provider, auth, and features {} block.

🧪 Testing

The offline proof gate is what this module guarantees:

  • terraform init -backend=false — resolves the pinned azurerm ~> 4.0 provider without a backend.
  • terraform validate — proves the configuration is internally consistent and type-correct against the provider schema; the deeply-typed object() inputs and validation {} rules surface malformed input at parse time.
  • terraform fmt -check — enforces canonical formatting.

Neither validate nor fmt calls Azure. Only terraform plan (run by a human, from CI) exercises the ARM API.


💬 Example Output

$ terraform output
id                     = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-eventing-prod/providers/Microsoft.EventGrid/systemTopics/egst-orders-storage"
name                   = "egst-orders-storage"
metric_arm_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-eventing-prod/providers/Microsoft.EventGrid/systemTopics/egst-orders-storage"
identity_principal_id  = "11111111-2222-3333-4444-555555555555"
identity_tenant_id     = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

🔍 Troubleshooting

Symptom Cause Fix
provider "azurerm" ... features initialization error No caller-side features {} block Add provider "azurerm" { features {} } to the root module.
Plan shows the system topic being replaced A force-new field changed (name, location, resource_group_name, source_arm_resource_id, or topic_type) Confirm the change is intended; these fields cannot change in place.
Create fails referencing the source resource topic_type does not match the kind of source_arm_resource_id, or the caller lacks read access on the source Align the type with the source kind and grant Reader on the source resource.
identity.type must be exactly "SystemAssigned", "UserAssigned" or "SystemAssigned, UserAssigned"... identity.type value not in the legal set Use SystemAssigned, UserAssigned, or exactly "SystemAssigned, UserAssigned".
identity.identity_ids is required when identity.type includes "UserAssigned"... type includes UserAssigned but no identity_ids supplied Provide at least one user-assigned identity resource ID.
Subscription delivery is denied The topic's identity lacks a role on the delivery target Grant the target's data-plane role to identity_principal_id at the target's scope.

🔗 Related Docs

  • Terraform Registry — azurerm_eventgrid_system_topic
  • Microsoft Learn — Azure Event Grid system topics
  • Microsoft Learn — System topics and event sources
  • Sibling modules — terraform-azurerm-resource-group, terraform-azurerm-storage-account, terraform-azurerm-user-assigned-identity, terraform-azurerm-role-assignments, terraform-azurerm-eventgrid-system-topic-event-subscription (the subscriptions on this topic — it also selects which of this topic's managed identities delivers events), terraform-azurerm-eventgrid-event-subscription (the generic form, which addresses its parent by scope instead)
  • This module's SCOPE.md

💙 "Infrastructure as Code should be standardized, consistent, and secure."