# 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

```hcl
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](https://github.com/saif-corp/cloud-foundations/pull/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.

<!-- BEGIN_TF_DOCS -->
## Providers

| Name | Version |
|------|---------|
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | >= 4.0, < 5.0 |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_context"></a> [context](#input\_context) | Platform context from the environment module | `any` | n/a | yes |
| <a name="input_identity"></a> [identity](#input\_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 |
| <a name="input_queues"></a> [queues](#input\_queues) | Map of queue definitions, keyed by a stable identity name (e.g. "intake", "retries"). Each<br/>queue is provisioned independently on the shared namespace, with its own DLQ/poison-message<br/>configuration and RBAC. Entries default to a single "default" queue when unset. | <pre>map(object({<br/>    # Number of delivery attempts before a message is automatically dead-lettered as a poison message.<br/>    max_delivery_count = optional(number, 10)<br/><br/>    # ISO 8601 duration a message is locked for a receiving consumer before it becomes available again.<br/>    lock_duration = optional(string, "PT30S")<br/><br/>    # ISO 8601 duration for the default time-to-live of messages on the queue. Expired messages<br/>    # are dead-lettered when dead_lettering_on_message_expiration is true, otherwise discarded.<br/>    default_message_ttl = optional(string, "P14D")<br/><br/>    # Whether expired messages are moved to the dead-letter queue instead of being discarded.<br/>    dead_lettering_on_message_expiration = optional(bool, true)<br/><br/>    # Whether the queue detects and drops duplicate messages sent within duplicate_detection_history_time_window.<br/>    #<br/>    # ⚠️ This is a create-only setting on the underlying azurerm_servicebus_queue resource: Azure<br/>    # Service Bus does not support enabling/disabling duplicate detection on an existing queue, so<br/>    # changing this value after creation forces Terraform to destroy and recreate the queue,<br/>    # discarding any live and dead-lettered messages still on it. Treat it as fixed at creation<br/>    # time; if you need to change it, provision a new queue under a new key, drain/migrate<br/>    # in-flight messages to it, then remove the old key once traffic has cut over. See<br/>    # docs/build/eventing/servicebus-queue.md#changing-requires_duplicate_detection.<br/>    requires_duplicate_detection = optional(bool, false)<br/><br/>    # ISO 8601 duration window used for duplicate detection. Only applies when requires_duplicate_detection is true.<br/>    duplicate_detection_history_time_window = optional(string, "PT10M")<br/><br/>    # Maximum queue size in megabytes.<br/>    max_size_in_megabytes = optional(number, 1024)<br/><br/>    # ---- DLQ access ----<br/>    # Map of identity keys to Entra ID object IDs — not group names. Group lookups via a<br/>    # `data "azuread_group"` source execute at plan time and fail the Organization Policy Check<br/>    # (see the blob storage module's contributor_group_id /<br/>    # docs/build/data/storage-account.md). Look the object ID up once (Entra admin<br/>    # center or `az ad group show`) and paste the literal GUID.<br/>    dlq_reader_identities  = optional(map(string), {})<br/>    dlq_manager_identities = optional(map(string), {})<br/>  }))</pre> | <pre>{<br/>  "default": {}<br/>}</pre> | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_app_settings"></a> [app\_settings](#output\_app\_settings) | App settings map for web app configuration — one ServiceBusQueueName\_\_{key} entry per queue in<br/>var.queues. No connection entry is emitted: the namespace's fully-qualified host is published<br/>centrally to shared App Configuration as ConnectionStrings:messaging (see<br/>cloud-foundations#134) and read via AddAzureServiceBusClient(connectionName: "messaging").<br/>App Configuration is loaded after the App Service environment-variable provider and therefore<br/>wins, so a connection entry emitted here could not override the central value anyway. |
| <a name="output_namespace_name"></a> [namespace\_name](#output\_namespace\_name) | The Service Bus namespace name the queues were created on |
| <a name="output_queue_ids"></a> [queue\_ids](#output\_queue\_ids) | Map of queue key to Service Bus queue resource ID |
| <a name="output_queue_names"></a> [queue\_names](#output\_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)    
<!-- END_TF_DOCS -->

---

[View source on GitHub](https://github.com/saif-corp/forge/blob/main/src/terraform/saif-resources/modules/servicebus-queue/README.md)
