Managing Azure Event Grid with Terraform

Learn how to set up and manage Azure Event Grid using Terraform, including topics, subscriptions, and event handling

Azure Event Grid is a fully managed event routing service. This guide demonstrates how to set up and manage Azure Event Grid using Terraform.

Video Tutorial

Learn more about managing Azure Event Grid with Terraform in this comprehensive video tutorial:

Prerequisites

  • Azure CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Resource group created
  • Understanding of event-driven architecture

Project Structure

terraform-azure-eventgrid/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   └── eventgrid/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── policies/
    └── security.json

Event Grid Configuration

Create modules/eventgrid/main.tf:

# System Topic
resource "azurerm_eventgrid_system_topic" "storage" {
  name                   = "${var.project_name}-storage-events"
  location              = var.location
  resource_group_name   = var.resource_group_name
  source_arm_resource_id = azurerm_storage_account.main.id
  topic_type            = "Microsoft.Storage.StorageAccounts"

  identity {
    type = "SystemAssigned"
  }

  tags = var.tags
}

# Custom Topic
resource "azurerm_eventgrid_topic" "main" {
  name                = "${var.project_name}-topic"
  location            = var.location
  resource_group_name = var.resource_group_name

  input_schema = "EventGridSchema"

  public_network_access_enabled = false

  inbound_ip_rule {
    ip_mask = var.allowed_ip_range
    action  = "Allow"
  }

  identity {
    type = "SystemAssigned"
  }

  tags = var.tags
}

# Domain
resource "azurerm_eventgrid_domain" "main" {
  name                = "${var.project_name}-domain"
  location            = var.location
  resource_group_name = var.resource_group_name

  input_schema = "EventGridSchema"

  public_network_access_enabled = false

  identity {
    type = "SystemAssigned"
  }

  tags = var.tags
}

# Domain Topic
resource "azurerm_eventgrid_domain_topic" "main" {
  name                = "orders"
  domain_name         = azurerm_eventgrid_domain.main.name
  resource_group_name = var.resource_group_name
}

# Event Subscription (to Function)
resource "azurerm_eventgrid_event_subscription" "function" {
  name                = "${var.project_name}-function-sub"
  scope               = azurerm_eventgrid_topic.main.id
  included_event_types = ["OrderCreated", "OrderUpdated"]

  azure_function_endpoint {
    function_id = "${var.function_app_id}/functions/ProcessOrder"
    max_events_per_batch = 1
    preferred_batch_size_in_kilobytes = 64
  }

  advanced_filter {
    string_begins_with {
      key    = "Subject"
      values = ["orders/high-priority"]
    }
  }

  retry_policy {
    max_delivery_attempts = 30
    event_time_to_live   = 1440
  }

  # Access to the dead-letter container is granted via dead_letter_identity
  # (RBAC on the storage account) - there's no separate SAS URL attribute;
  # a SAS token and a managed identity are two different auth mechanisms.
  dead_letter_identity {
    type = "SystemAssigned"
  }

  storage_blob_dead_letter_destination {
    storage_account_id         = azurerm_storage_account.dlq.id
    storage_blob_container_name = "deadletter"
  }
}

# Event Subscription (to Service Bus)
resource "azurerm_eventgrid_event_subscription" "servicebus" {
  name                = "${var.project_name}-servicebus-sub"
  scope               = azurerm_eventgrid_topic.main.id
  included_event_types = ["OrderCancelled"]

  service_bus_topic_endpoint_id = azurerm_servicebus_topic.main.id

  advanced_filter {
    string_in {
      key    = "Data.reason"
      values = ["OutOfStock", "CustomerRequest"]
    }
  }

  delivery_property {
    header_name = "OrderId"
    type        = "Dynamic"
    source_field = "data.orderId"
  }
}

# Event Subscription (to Event Hub)
resource "azurerm_eventgrid_event_subscription" "eventhub" {
  name                = "${var.project_name}-eventhub-sub"
  scope               = azurerm_eventgrid_topic.main.id
  included_event_types = ["OrderShipped"]

  eventhub_endpoint_id = azurerm_eventhub.main.id

  advanced_filter {
    number_greater_than {
      key    = "Data.amount"
      value  = 1000
    }
  }
}

# Event Subscription (to Storage Queue)
resource "azurerm_eventgrid_event_subscription" "queue" {
  name                = "${var.project_name}-queue-sub"
  scope               = azurerm_eventgrid_topic.main.id
  included_event_types = ["OrderReturned"]

  storage_queue_endpoint {
    storage_account_id = azurerm_storage_account.main.id
    queue_name        = "returns"
  }
}

Network Configuration

  1. Virtual Network Integration
resource "azurerm_virtual_network" "main" {
  name                = "${var.project_name}-vnet"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = ["10.0.0.0/16"]

  tags = var.tags
}

# The inline subnet block on azurerm_virtual_network is deprecated - use a
# standalone azurerm_subnet resource instead
resource "azurerm_subnet" "eventgrid_subnet" {
  name                 = "eventgrid-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]
  service_endpoints = ["Microsoft.EventGrid"]
}

resource "azurerm_private_endpoint" "eventgrid" {
  name                = "${var.project_name}-pe"
  location            = var.location
  resource_group_name = var.resource_group_name
  subnet_id           = azurerm_subnet.eventgrid_subnet.id

  private_service_connection {
    name                           = "${var.project_name}-psc"
    private_connection_resource_id = azurerm_eventgrid_topic.main.id
    is_manual_connection          = false
    subresource_names            = ["topic"]
  }

  private_dns_zone_group {
    name                 = "default"
    private_dns_zone_ids = [azurerm_private_dns_zone.eventgrid.id]
  }
}

resource "azurerm_private_dns_zone" "eventgrid" {
  name                = "privatelink.eventgrid.azure.net"
  resource_group_name = var.resource_group_name
}

resource "azurerm_private_dns_zone_virtual_network_link" "eventgrid" {
  name                  = "${var.project_name}-vnet-link"
  resource_group_name   = var.resource_group_name
  private_dns_zone_name = azurerm_private_dns_zone.eventgrid.name
  virtual_network_id    = azurerm_virtual_network.main.id
}

Security Configuration

  1. Role Assignments
resource "azurerm_role_assignment" "eventgrid_publisher" {
  scope                = azurerm_eventgrid_topic.main.id
  role_definition_name = "EventGrid Data Sender"
  principal_id         = var.publisher_principal_id
}

resource "azurerm_role_assignment" "eventgrid_subscriber" {
  scope                = azurerm_eventgrid_topic.main.id
  role_definition_name = "EventGrid Data Receiver"
  principal_id         = var.subscriber_principal_id
}
  1. IP Rules

IP filtering is configured via an inbound_ip_rule block on the topic itself (shown above under Custom Topic) - there’s no standalone IP-rule resource for Event Grid topics.

Monitoring Configuration

  1. Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "eventgrid" {
  name                       = "${var.project_name}-diag"
  target_resource_id        = azurerm_eventgrid_topic.main.id
  log_analytics_workspace_id = var.log_analytics_workspace_id

  log {
    category = "DeliveryFailures"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  metric {
    category = "AllMetrics"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }
}

resource "azurerm_monitor_metric_alert" "eventgrid" {
  name                = "${var.project_name}-delivery-alert"
  resource_group_name = var.resource_group_name
  scopes              = [azurerm_eventgrid_topic.main.id]
  description         = "Alert when event delivery fails"

  criteria {
    metric_namespace = "Microsoft.EventGrid/topics"
    metric_name      = "UnmatchedEventCount"
    aggregation      = "Total"
    operator         = "GreaterThan"
    threshold        = 10
  }

  action {
    action_group_id = var.action_group_id
  }
}

Advanced Features

  1. Input Mapping

Input mapping is configured on the topic that receives custom-schema events, not on an event subscription:

resource "azurerm_eventgrid_topic" "custom_schema" {
  name                = "${var.project_name}-custom-topic"
  location            = var.location
  resource_group_name = var.resource_group_name

  input_schema = "CustomEventSchema"

  input_mapping_fields {
    topic      = "subject"
    event_type = "eventType"
    event_time = "eventTime"
    data_version = "dataVersion"
  }

  input_mapping_default_values {
    data_version = "1.0"
    subject     = "DefaultSubject"
  }
}
  1. Batching
resource "azurerm_eventgrid_event_subscription" "batch" {
  name                = "${var.project_name}-batch-sub"
  scope               = azurerm_eventgrid_topic.main.id

  webhook_endpoint {
    url = "https://example.com/api/events"
    max_events_per_batch = 10
    preferred_batch_size_in_kilobytes = 64
  }
}

Best Practices

  1. Performance

    • Use appropriate endpoints
    • Configure batching
    • Implement retry policies
    • Monitor delivery rates
  2. Security

    • Enable private endpoints
    • Use RBAC
    • Implement IP rules
    • Enable authentication
  3. Reliability

    • Configure dead-lettering
    • Set retry policies
    • Monitor failures
    • Use filtering
  4. Cost Optimization

    • Clean up subscriptions
    • Use appropriate endpoints

Conclusion

You’ve learned how to set up and manage Azure Event Grid using Terraform. This setup provides:

  • Event routing
  • Security and filtering
  • Monitoring and alerts
  • Dead-letter handling

Remember to:

  • Monitor delivery rates
  • Review security settings
  • Handle dead-letters
  • Update access controls