Skip to content

saif-resources / servicebus-queue

⚠️ Experimental: This module is experimental and still evolving — inputs, outputs, and behavior may change without a deprecation period. Pin your module version carefully and expect rough edges before relying on it in production.

App-scoped Azure Service Bus queue module. Provisions one or more queues on the shared, per-environment/per-domain Service Bus namespace for an app's own internal producer/consumer messaging, with least-privilege RBAC scoped to each queue itself.

Usage

module "servicebus_queue" {
  source  = "app.terraform.io/SAIFCorp/resources/saif//modules/servicebus-queue"
  version = "~> 3.9.0"

  context  = module.environment.context
  identity = module.identity.identity

  # A single, default queue is provisioned automatically if `queues` is left unset.
  # To provision multiple independent queues, key the map by a stable name:
  queues = {
    intake = {
      max_delivery_count = 10
    }
    retries = {
      max_delivery_count                   = 20
      dead_lettering_on_message_expiration = true
    }
  }
}

# Compose app_settings
module "webapp" {
  app_settings = merge(
    module.servicebus_queue.app_settings,
    # ...
  )
}

Inputs

Name Description Required
context Platform context from environment module yes
identity Identity bundle from identity module yes
queues Map of queue definitions keyed by a stable identity name (default: a single default queue) — see below no

Each entry in queues supports:

Name Description Default
max_delivery_count Poison-message threshold before dead-lettering 10
lock_duration ISO 8601 message lock duration PT30S
default_message_ttl ISO 8601 default message TTL P14D
dead_lettering_on_message_expiration Dead-letter expired messages instead of discarding them true
requires_duplicate_detection Enable duplicate detection false
duplicate_detection_history_time_window ISO 8601 duplicate-detection window PT10M
max_size_in_megabytes Maximum queue size 1024
dlq_reader_identities Map of identity keys to Entra ID object IDs granted DLQ read access on this queue {}
dlq_manager_identities Map of identity keys to Entra ID object IDs granted DLQ manage access on this queue {}

Outputs

Name Description
queue_names Map of queue key to Service Bus queue name
queue_ids Map of queue key to Service Bus queue resource ID
namespace_name The Service Bus namespace name the queues were created on
app_settings One ServiceBusQueueName__{key} entry per queue in queues

What it creates

  • One Service Bus queue per entry in queues on the shared namespace (DLQ, retry, and duplicate-detection independently configurable per queue)
  • RBAC: Azure Service Bus Data Sender + Azure Service Bus Data Receiver → the app's App Registration service principal, scoped to each queue only
  • Optional RBAC: Azure Service Bus Data Receiver / Azure Service Bus Data Owner on a queue for its dlq_reader_identities / dlq_manager_identities (ops/on-call access to poison messages)

RBAC notes

The app's App Registration service principal receives Sender and Receiver on every queue, scoped to that queue's own azurerm_servicebus_queue.this[key].id rather than the namespace — least privilege for queues that are private to a single app. Only the SP is granted access: the app runtime authenticates via EnvironmentCredential using the SP's client ID (see the identity module's AZURE_CLIENT_ID app setting), so the UAMI is never used as a data-plane credential here and is intentionally not granted queue access. This is intentionally distinct from the namespace-wide grant proposed for generated APIs publishing to shared topics (see #1144); that grant serves many operations across shared topics, while these queues are single-app resources where the tighter, per-queue scope is both possible and preferable.

Azure RBAC has no separate scope for a queue's dead-letter sub-queue — granting Receiver/Owner for DLQ access also permits receiving/managing live messages on the queue itself. dlq_reader_identities/dlq_manager_identities take Entra ID object IDs directly (not group names resolved via a data "azuread_group" lookup), because such lookups execute at plan time and fail the Organization Policy Check — the same convention as the storage module's contributor_group_id (see the Service Bus Queue development guide).

Role assignments are deduplicated by resolved principal ID, not by identity alias: Azure rejects a duplicate (principal_id, role, scope) role assignment, so two dlq_reader_identities/dlq_manager_identities aliases that resolve to the same object ID collapse to a single grant, and any dlq_reader_identities alias that duplicates the app's own SP is skipped entirely (it's already granted Receiver above).

Connection string

The namespace's fully-qualified host is published centrally to shared App Configuration as ConnectionStrings:messaging (see cloud-foundations#134) — apps bind with AddAzureServiceBusClient(connectionName: "messaging") and do not need a per-app connection string, so this module's app_settings output only carries the queue name(s). messaging is the universal connection name used across all consumers of this module, so it is not configurable.

The module deliberately emits no connection entry of its own. AddAzureDefaults() loads App Configuration after the App Service environment-variable provider, and the last configuration provider wins, so a ConnectionStrings__messaging app setting could not override the centrally published value anyway.

Providers

Name Version
azurerm >= 4.0, < 5.0

Inputs

Name Description Type Default Required
context Platform context from the environment module any n/a yes
identity Identity bundle from the identity module. The app's App Registration service principal is granted Azure Service Bus Data Sender and Data Receiver on every queue in var.queues, scoped to each queue only — the UAMI is not granted access, since the app runtime authenticates as the SP via EnvironmentCredential. any n/a yes
queues Map of queue definitions, keyed by a stable identity name (e.g. "intake", "retries"). Each
queue is provisioned independently on the shared namespace, with its own DLQ/poison-message
configuration and RBAC. Entries default to a single "default" queue when unset.
map(object({
# Number of delivery attempts before a message is automatically dead-lettered as a poison message.
max_delivery_count = optional(number, 10)

# ISO 8601 duration a message is locked for a receiving consumer before it becomes available again.
lock_duration = optional(string, "PT30S")

# ISO 8601 duration for the default time-to-live of messages on the queue. Expired messages
# are dead-lettered when dead_lettering_on_message_expiration is true, otherwise discarded.
default_message_ttl = optional(string, "P14D")

# Whether expired messages are moved to the dead-letter queue instead of being discarded.
dead_lettering_on_message_expiration = optional(bool, true)

# Whether the queue detects and drops duplicate messages sent within duplicate_detection_history_time_window.
#
# ⚠️ This is a create-only setting on the underlying azurerm_servicebus_queue resource: Azure
# Service Bus does not support enabling/disabling duplicate detection on an existing queue, so
# changing this value after creation forces Terraform to destroy and recreate the queue,
# discarding any live and dead-lettered messages still on it. Treat it as fixed at creation
# time; if you need to change it, provision a new queue under a new key, drain/migrate
# in-flight messages to it, then remove the old key once traffic has cut over. See
# docs/build/eventing/servicebus-queue.md#changing-requires_duplicate_detection.
requires_duplicate_detection = optional(bool, false)

# ISO 8601 duration window used for duplicate detection. Only applies when requires_duplicate_detection is true.
duplicate_detection_history_time_window = optional(string, "PT10M")

# Maximum queue size in megabytes.
max_size_in_megabytes = optional(number, 1024)

# ---- DLQ access ----
# Map of identity keys to Entra ID object IDs — not group names. Group lookups via a
# data "azuread_group" source execute at plan time and fail the Organization Policy Check
# (see the blob storage module's contributor_group_id /
# docs/build/data/storage-account.md). Look the object ID up once (Entra admin
# center or az ad group show) and paste the literal GUID.
dlq_reader_identities = optional(map(string), {})
dlq_manager_identities = optional(map(string), {})
}))
{
"default": {}
}
no

Outputs

Name Description
app_settings App settings map for web app configuration — one ServiceBusQueueName__{key} entry per queue in
var.queues. No connection entry is emitted: the namespace's fully-qualified host is published
centrally to shared App Configuration as ConnectionStrings:messaging (see
cloud-foundations#134) and read via AddAzureServiceBusClient(connectionName: "messaging").
App Configuration is loaded after the App Service environment-variable provider and therefore
wins, so a connection entry emitted here could not override the central value anyway.
namespace_name The Service Bus namespace name the queues were created on
queue_ids Map of queue key to Service Bus queue resource ID
queue_names Map of queue key to Service Bus queue name

Resources

  • resource.azurerm_role_assignment.dlq_manager (/terraform-docs/modules/servicebus-queue/main.tf#56)
  • resource.azurerm_role_assignment.dlq_reader (/terraform-docs/modules/servicebus-queue/main.tf#49)
  • resource.azurerm_role_assignment.receiver (/terraform-docs/modules/servicebus-queue/main.tf#41)
  • resource.azurerm_role_assignment.sender (/terraform-docs/modules/servicebus-queue/main.tf#34)
  • resource.azurerm_servicebus_queue.this (/terraform-docs/modules/servicebus-queue/main.tf#18)

View source on GitHub