Skip to main content

Managing Azure Service Bus using Terraform

Overview

Azure Service Bus is an enterprise service broker with message queues and publish-subscribe topics. It is used to decouple applications, transferring data between services using messages.

The monthly cost of Premium instances is quite high, so it may be tempting to create a single instance for all domains that are part of the same product. Luckily, sharing a single instance among teams is not an issue, as the Service Bus offers fine granularity in terms of access management (IAM).

This document offers an overview on how to provision a new Namespace, but also goes deep into explaining the best practices in permission separation and how and where to write Terraform code. Therefore, the document could be useful for anybody interested in using Service Bus, even if not shared among domain teams.

Creating a Service Bus Namespace

The Service Bus Namespace can be provisioned by using the DX Terraform module Azure Service Bus Namespace, which hides complexities such as networking, authentication, and scaling.

module "service_bus_01" {
source = "pagopa-dx/azure-service-bus-namespace/azurerm"
version = "~>0.0"

environment = {
prefix = "dx"
env_short = "d"
location = "italynorth"
app_name = "test"
instance_number = "01"
}

resource_group_name = azurerm_resource_group.example.name

subnet_pep_id = data.azurerm_subnet.pep.id

tier = "l"

tags = local.tags
}

The module disables access keys, so authentication is handled only by Entra ID.

The team or pipeline that manages the Service Bus Namespace infrastructure must have the Contributor role assigned on the whole service. This is necessary, for example, to let GitHub Actions create and manage entities such as queues, topics, and subscriptions within the Namespace.

If you are using GitHub Actions to manage the Service Bus Namespace, it is recommended to leverage the azure-github-environment-bootstrap module. By specifying the sbns_id variable in the module configuration, you can ensure that the necessary roles are automatically assigned to your repository's GitHub Actions workflows.

If, for any exceptional reason you need to configure permissions manually, you can use the azurerm_role_assignment resource to assign the Contributor role to a Principal ID. For example:

data "azuread_group" "adgroup_domain_devs" {
display_name = "<your-team>"
}

resource "azurerm_role_assignment" "example" {
scope = module.service_bus_01.id
role_definition_name = "Contributor"
principal_id = data.azuread_group.adgroup_domain_devs.object_id
description = "This role allows my team to manage entities"
}

Creating Entities and Managing Their Access

Service Bus entities can be created using Terraform resources for queues, topics, and subscriptions.

However, the DX Terraform module dx-azure-role-assignments is helpful to assign roles within those entities, abstracting the complexity of the role choice. It only requires the Principal ID of the resource that needs to access the Service Bus; this could be either a Managed Identity associated with your resource or an Entra ID group.

The following sections show guidance and examples for each entity type.

Queues

Queues offer First In, First Out (FIFO) message delivery to one or more competing consumers. That is, receivers typically receive and process messages in the order in which they were added to the queue. And, only one message consumer receives and processes each message. Source

As consumers compete for the same message, it is recommended to manage queue access in a single Terraform codebase, so that there is always a clear overview of which is consuming messages in a given queue. Therefore, the team owner of the queue should create it and then manage its access in the same configuration, even if the consumer is in another domain or cross-subscription scenario. If multiple teams read events from the same queue without being aware of them, hard-to-detect concurrency problems could occur.

The following example does:

  • Create a queue
  • Assign the Owner role to an Entra ID team (mandatory if you need to explore and modify the queue, e.g., from Azure Portal)
  • Assign the Writer role to the app which sends events
  • Assign the Reader role to the app which listens to events
resource "azurerm_servicebus_queue" "example" {
name = "example-queue"
namespace_id = module.service_bus_01.id
}

module "queue_team" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <your_team_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "owner"
description = "This role allows managing the given queue"
queue_names = [azurerm_servicebus_queue.example.name]
}
]
}

module "queue_producer" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <producer_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "writer"
description = "This role allows sending messages to the given queue"
queue_names = [azurerm_servicebus_queue.example.name]
}
]
}

module "queue_consumer" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <consumer_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "reader"
description = "This role allows receiving messages from the given queue"
queue_names = [azurerm_servicebus_queue.example.name]
}
]
}

Topics and Subscriptions

In contrast to queues, topics and subscriptions provide a one-to-many form of communication in a publish and subscribe pattern. It's useful for scaling to large numbers of recipients. Each published message is made available to each subscription registered with the topic. Publishers send a message to a topic, and one or more subscribers receive a copy of the message. Source

Since topics replicate messages to each subscriber, the team owner of the entity will only need to define the topic itself and give the Writer role to producer service in order to post messages.

The following example does:

  • Create a topic
  • Assign the Owner role to an Entra ID team (mandatory if you need to explore and modify the topic, e.g., from Azure Portal)
  • Assign the Writer role to the app which sends events to the topic
resource "azurerm_servicebus_topic" "example" {
name = "example-topic"
namespace_id = module.service_bus_01.id
}

module "topic_team" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <your_team_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "owner"
description = "This role allows managing the given topic"
topic_names = [azurerm_servicebus_topic.example.name]
}
]
}

module "topic_producer" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <producer_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "writer"
description = "This role allows sending messages to the given topic"
topic_names = [azurerm_servicebus_topic.example.name]
}
]
}

Subscriptions' consumers, on the other hand, will be managed by the individual subscription owners, assigning the Reader role to consumers. In cross-domain scenarios, the Terraform code can be written in the consumer's repository. In cross-subscription cases, refer to the general guidelines.

The following example does:

  • Create a subscription
  • Assign the Owner role to an Entra ID team (mandatory if you need to explore and modify the subscription, e.g., from Azure Portal)
  • Assign the Reader role to the app which receives events
# If you are the owner of the topic, you should already have this code:
resource "azurerm_servicebus_topic" "example" {
name = "example-topic"
namespace_id = module.service_bus_01.id
}

# Instead, if you are consuming events of another team's topic use:
data "azurerm_servicebus_topic" "example" {
name = "example-topic"
namespace_id = module.service_bus_01.id
}

resource "azurerm_servicebus_subscription" "example" {
name = "example-sub"
topic_id = (data.)azurerm_servicebus_topic.example.id
max_delivery_count = 1
}

module "subscription_team" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <your_team_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "owner"
description = "This role allows managing the given subscription"
subscriptions = {
example-topic = [azurerm_servicebus_subscription.example.name],
}
}
]
}

module "subscription_consumer" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~>1.0"

principal_id = <consumer_principal_id>
subscription_id = data.azurerm_subscription.current.subscription_id

service_bus = [
{
namespace_name = module.service_bus_01.name
resource_group_name = module.service_bus_01.resource_group_name
role = "reader"
description = "This role allows receiving messages from the given subscription"
subscriptions = {
example-topic = [azurerm_servicebus_subscription.example.name],
}
}
]
}