In this post we are going to cover security, networking and disaster recovery in Azure Service Bus. These are 7 real Service Bus interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: a bank needs a secure, highly available messaging platform across two regions.
Security here means three things: who can send and receive, from which network, and with which keys the messages are encrypted. Disaster recovery means what happens when a zone or a whole region goes down. Interviewers ask about this because these settings are hard to change later, and a wrong answer in production means a leaked key, an open namespace, or lost messages.
Part 9 of the Real Time Azure Service Bus Interview Questions and Answers series on TechBrothersIT.
- The short answer
- Let's set up the project
- Question 1: Leadership asks how services should authenticate to Service Bus. SAS keys or Microsoft Entra ID?
- Question 2: A function needs to send messages with no secrets in code. How do managed identities and Service Bus roles work?
- Question 3: Security wants no public access to the namespace. What networking options do you set up?
- Question 4: A teammate asks what happens if an availability zone fails. How do zones protect Service Bus?
- Question 5: The team needs a plan for a full region outage. What is the difference between Geo-disaster recovery and Geo-replication?
- Question 6: Someone asks how to encrypt messages with the company's own keys. How does customer-managed key encryption work?
- Scenario 7: A bank needs a secure, highly available messaging platform across two regions. What do you set up?
- The full script
The short answer
Use Microsoft Entra ID with managed identities and the Azure Service Bus Data Sender / Receiver roles, then disable SAS (local auth). On Premium, close the namespace with private endpoints and public access disabled. Availability zones are built in on every tier; Geo-DR replicates configuration only, Geo-replication (Premium) replicates the messages too, and consumers stay idempotent either way.
Let's set up the project
We run Microsoft's official Service Bus emulator locally with SQL Server, and small Python scripts that use the azure-servicebus client, so every output is real. Most of this topic, like Entra ID, private endpoints and Geo-replication, doesn't run on the emulator, so it is shown as code. Config.json creates four queues, orders, payments with duplicate detection, orders-dr and transfers-in, and a transfers topic with three subscriptions: audit, fraud and ledger.
{
"UserConfig": {
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [
{
"Name": "orders",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
},
{
"Name": "payments",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": true,
"RequiresSession": false
}
},
{
"Name": "orders-dr",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
},
{
"Name": "transfers-in",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
}
],
"Topics": [
{
"Name": "transfers",
"Properties": {
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"RequiresDuplicateDetection": false
},
"Subscriptions": [
{
"Name": "audit",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
},
{
"Name": "fraud",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
},
{
"Name": "ledger",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
}
]
}
]
}
],
"Logging": {
"Type": "File"
}
}
}Question 1: Leadership asks how services should authenticate to Service Bus. SAS keys or Microsoft Entra ID?
Microsoft Entra ID, wherever you can. Microsoft recommends it: role-based access, one identity per app, and with a managed identity, no secret at all. SAS keys are hard to rotate and leave no audit trail.
| SAS key or token | Microsoft Entra ID | |
|---|---|---|
| Secret to store | yes: a key or a token | no: a managed identity |
| Rights | Send, Listen, Manage per policy | Data Sender, Data Receiver, Data Owner roles |
| Rotate or revoke | regenerate the key, re-sign every token | remove one role assignment |
| Audit: who did it? | no audit trail per caller | one identity per app |
Let's look at what a SAS connection string holds, and the fields of a SAS token signed locally. Then we try a wrong key. We run python sas_token.py and python wrong_key.py.
import base64, hashlib, hmac, os, urllib.parse
conn = os.environ["SERVICEBUS_CONNECTION_STRING"]
parts = dict(p.split("=", 1) for p in conn.strip(";").split(";"))
for name, value in parts.items():
shown = "*****" if name == "SharedAccessKey" else value
print(f"{name:22} {shown}")
def sas_token(uri, key_name, key, expiry):
sr = urllib.parse.quote_plus(uri)
to_sign = f"{sr}\n{expiry}".encode()
sig = hmac.new(key.encode(), to_sign, hashlib.sha256).digest()
sig = urllib.parse.quote_plus(base64.b64encode(sig))
return (f"SharedAccessSignature sr={sr}&sig={sig}"
f"&se={expiry}&skn={key_name}")
token = sas_token("https://tbshop-bus.servicebus.windows.net/orders",
"orders-send", "SEND_KEY", 1798761600) # 2027-01-01
for field in token.split(" ", 1)[1].split("&"):
print("token", field)import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
conn = os.environ["SERVICEBUS_CONNECTION_STRING"]
wrong = conn.replace("SAS_KEY_VALUE", "NOT_THE_REAL_KEY")
with ServiceBusClient.from_connection_string(wrong) as client:
with client.get_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage("order 1001"))
print("wrong key: send accepted (the emulator checks no keys)")$ python sas_token.py
Endpoint sb://localhost
SharedAccessKeyName RootManageSharedAccessKey
SharedAccessKey *****
UseDevelopmentEmulator true
token sr=https%3A%2F%2Ftbshop-bus.servicebus.windows.net%2Forders
token sig=KWpu7bMVB3a22nz4GekH27dceAI58QGkeA4wWe5SrVA%3D
token se=1798761600
token skn=orders-send
$ python wrong_key.py
wrong key: send accepted (the emulator checks no keys)The connection string has the Endpoint, the policy name, here RootManageSharedAccessKey, the key itself, which we mask as *, and the emulator flag UseDevelopmentEmulator true. That root policy manages the whole namespace. Never ship it in an app.
The token is signed with a policy key. It has four fields: sr is the resource URI, sig is the signature, se=1798761600 is the expiry in seconds since 1970, and skn=orders-send is the key name. Policies grant Send, Listen or Manage.
One honest note: the emulator doesn't check keys. The wrong key prints send accepted, so we can't show a refusal here. In Azure, a wrong key or an expired token is rejected.
Once every app uses Entra ID, switch SAS off:
# after every app uses Microsoft Entra ID: switch SAS off
az servicebus namespace update --resource-group rg-shop \
--name tbshop-bus --disable-local-auth trueEntra ID everywhere, SAS only where a client can't use it, then disable local authentication. A built-in Azure Policy can enforce that.
Question 2: A function needs to send messages with no secrets in code. How do managed identities and Service Bus roles work?
A managed identity is an identity Azure gives the compute itself, here a function app. At run time the code gets a token from Entra ID, so there is nothing to store or leak.
Here is the flow:
orders-func- managed identity: no secret- Microsoft Entra ID - issues a token at run time
ordersqueue - role: Data Sender
In Python only the credential changes: the fully qualified namespace plus DefaultAzureCredential, instead of a connection string. In Azure it finds the managed identity, and locally it uses your login. The emulator has no Entra ID, so this is code only:
from azure.identity import DefaultAzureCredential
from azure.servicebus import ServiceBusClient, ServiceBusMessage
# in production: ManagedIdentityCredential() is the stricter choice
client = ServiceBusClient(
fully_qualified_namespace="tbshop-bus.servicebus.windows.net",
credential=DefaultAzureCredential()) # no connection string
with client, client.get_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage('{"order_id": 1001}'))Then we grant a data role:
| Built-in role | Allows | Give it to |
|---|---|---|
| Azure Service Bus Data Sender | send | publishers |
| Azure Service Bus Data Receiver | receive | consumers |
| Azure Service Bus Data Owner | full access | admins and automation only |
Here the function gets Data Sender on the orders queue only. Grant the narrowest scope. For a single subscription, use the CLI, because the portal can't do it.
PRINCIPAL_ID=$(az functionapp identity assign \
--name orders-func --resource-group rg-shop \
--query principalId --output tsv)
QUEUE_ID=$(az servicebus queue show --resource-group rg-shop \
--namespace-name tbshop-bus --name orders \
--query id --output tsv)
az role assignment create \
--assignee-object-id "$PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "Azure Service Bus Data Sender" --scope "$QUEUE_ID"A new role assignment can take up to 5 minutes to apply. And a removed role keeps working until the token expires, 24 hours by default, unless the app restarts.
Question 3: Security wants no public access to the namespace. What networking options do you set up?
By default, a namespace is reachable from the internet if the caller authenticates. The answer is a Premium namespace with a private endpoint and public network access disabled. Private endpoints and VNets are Premium only.
Here is the target picture:
- Shop apps -
vnet-shop, VNet integration - Private endpoint - private IP, sub-resource:
namespace tbshop-bus(Premium) - public network access: Disabled- Internet - no way in
The private endpoint gives the namespace a private IP in your VNet. DNS goes through privatelink.servicebus.windows.net, so the normal host name resolves privately.
NS_ID=$(az servicebus namespace show -g rg-shop \
--name tbshop-bus --query id --output tsv)
az network private-endpoint create \
--name pe-tbshop-bus --resource-group rg-shop \
--vnet-name vnet-shop --subnet snet-pe \
--private-connection-resource-id "$NS_ID" \
--group-id namespace --connection-name tbshop-bus
az network private-dns zone create -g rg-shop \
--name privatelink.servicebus.windows.net
# then: link the zone to the VNet, add a DNS zone groupIn Bicep: public network access disabled, default action deny, and minimum TLS 1.2. If some public access must stay, the IP firewall allows listed ranges, on Standard too.
resource ns 'Microsoft.ServiceBus/namespaces@2024-01-01' = {
name: 'tbshop-bus'
location: location
sku: { name: 'Premium', tier: 'Premium', capacity: 1 }
properties: {
publicNetworkAccess: 'Disabled' // private endpoints only
disableLocalAuth: true // Microsoft Entra ID only
minimumTlsVersion: '1.2'
}
resource rules 'networkRuleSets' = {
name: 'default'
properties: {
publicNetworkAccess: 'Disabled'
defaultAction: 'Deny'
trustedServiceAccessEnabled: true
ipRules: [] // allowed IPv4 ranges, if public stays
}
}
}Trusted Microsoft services can bypass the firewall if you allow it:
- Event Grid, Stream Analytics - trusted
- Azure Monitor - diagnostic settings, alerts
App Service and Functions aren't trusted, so they use VNet integration.
The block is at the application layer, so an nslookup of the public endpoint can still succeed. Test a real send from inside the VNet, and remember: the network never replaces authentication.
Question 4: A teammate asks what happens if an availability zone fails. How do zones protect Service Bus?
Zones are built in. Basic, Standard and Premium all use availability zones where the region has them, automatically, at no extra cost. Messages are replicated synchronously across zones before the send is acknowledged, so no acknowledged message is lost.
Clients may see a few seconds of downtime, so they must retry. Python's defaults are three retries, exponential, with a backoff factor of 0.8, so waits of 1.6, 3.2 and 6.4 seconds.
The catch: if a send reached the broker but the acknowledgment was lost, the retry sends it again. zone_retry.py sends payment 7001 twice with the same message ID, payment-7001, into the payments queue, which has duplicate detection. We run python zone_retry.py.
import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"],
retry_total=3, retry_backoff_factor=0.8, # the defaults
retry_mode="exponential") # waits 1.6, 3.2, 6.4 s
payment = dict(body="payment 7001", message_id="payment-7001")
with client:
with client.get_queue_sender("payments") as sender:
sender.send_messages(ServiceBusMessage(**payment))
# zone blip: the ack is lost, so the app sends it again
sender.send_messages(ServiceBusMessage(**payment))
with client.get_queue_receiver("payments") as receiver:
got = receiver.receive_messages(max_message_count=10,
max_wait_time=3)
for msg in got:
receiver.complete_message(msg)
print("sent 2 times, received:", [str(m) for m in got])$ python zone_retry.py
sent 2 times, received: ['payment 7001']It prints sent 2 times, received: ['payment 7001']. One copy arrives.
Python doesn't set a message ID for you, so use a business key, or every retry looks new. We can't fail a zone on a laptop, so the zone part is from Microsoft's reliability guide.
A zone isn't a region. A full region outage needs Geo-disaster recovery or Geo-replication: the next question.
Question 5: The team needs a plan for a full region outage. What is the difference between Geo-disaster recovery and Geo-replication?
Both are Premium only. Geo-disaster recovery replicates configuration, never messages. Geo-replication replicates the messages and their state changes too, and Microsoft recommends it for most scenarios.
| Geo-disaster recovery | Geo-replication | |
|---|---|---|
| Replicates | configuration only: entities, settings | configuration and the message data |
| Clients use | an alias: one stable host name | the namespace host name: the primary |
| Failover | one way, by you; the pairing breaks | promote, by you: planned or forced |
| Not covered | messages, RBAC, network, private endpoints | large messages: not supported yet |
| Tier and status | Premium | Premium, generally available |
Geo-DR gives clients an alias, one stable host name over two paired namespaces. You trigger the failover, Azure never does. It is one way, and the pairing breaks. Role assignments and network settings aren't replicated.
# Geo-DR: pair, on the primary, with the secondary as partner
az servicebus georecovery-alias set --resource-group rg-shop \
--namespace-name tbshop-bus-east --alias tbshop-bus \
--partner-namespace "$SECONDARY_NAMESPACE_ID"
# outage: fail over, on the secondary (one time only)
az servicebus georecovery-alias fail-over --resource-group rg-shop \
--namespace-name tbshop-bus-west --alias tbshop-busGeo-replication keeps the namespace host name, pointing at the primary. It is synchronous, with RPO 0 and more latency, or asynchronous, up to a maximum lag. And you promote: planned waits for replication, forced goes now and can lose data.
# Geo-replication: promote the secondary region to primary
az servicebus namespace failover --resource-group rg-shop \
--namespace-name tbshop-bus --primary-location centralus
# planned by default; add --force for a forced promotionNow the Geo-DR effect, simulated. First, the consumer processes four orders. It writes each one keyed by message ID, in one commit, and completes only after the commit. We run python publish.py 1001 1004 and python consume.py.
import os, sys
from azure.servicebus import ServiceBusClient, ServiceBusMessage
first, last = int(sys.argv[1]), int(sys.argv[2])
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
with client, client.get_queue_sender("orders-dr") as sender:
sender.send_messages([ServiceBusMessage(f"order {n}",
message_id=f"order-{n}")
for n in range(first, last + 1)])
print(f"published order {first} to order {last}")import os, sqlite3
from azure.servicebus import ServiceBusClient
db = sqlite3.connect("shop.db")
db.execute("CREATE TABLE IF NOT EXISTS orders"
" (message_id TEXT PRIMARY KEY, body TEXT)")
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
with client, client.get_queue_receiver("orders-dr",
max_wait_time=3) as receiver:
for msg in receiver:
with db: # one commit per order
new = db.execute("INSERT OR IGNORE INTO orders"
" VALUES (?, ?)",
(msg.message_id, str(msg))).rowcount
print(msg, "processed" if new else "skipped: already done")
receiver.complete_message(msg) # after the commit
total = db.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
print("orders in the database:", total)$ python publish.py 1001 1004
published order 1001 to order 1004
$ python consume.py
order 1001 processed
order 1002 processed
order 1003 processed
order 1004 processed
orders in the database: 4All four are processed, and orders in the database: 4.
Then we restart the emulator. That stands in for the failover: same queue, no messages. The publisher re-sends 1003 to 1006 from its outbox, and the consumer runs again. We run python publish.py 1003 1006 and python consume.py.
$ python publish.py 1003 1006
published order 1003 to order 1006
$ python consume.py
order 1003 skipped: already done
order 1004 skipped: already done
order 1005 processed
order 1006 processed
orders in the database: 6The database already has 1003 and 1004, so they are skipped: already done. 1005 and 1006 are processed, and orders in the database: 6: six orders, none twice.
Duplicate detection and scheduling might not work, so idempotent consumers are the safety net.
Question 6: Someone asks how to encrypt messages with the company's own keys. How does customer-managed key encryption work?
Service Bus always encrypts data at rest with Microsoft-managed keys. A customer-managed key adds your own key from Azure Key Vault on top, on Premium only.
There are three pieces:
tbshop-bus- Premium namespace- Managed identity - get, wrapKey, unwrapKey, list
tbshop-kv- soft delete + purge protection
Microsoft's guide gives the identity access to the key with an access policy. The emulator has no encryption settings, so this is code. First, the identity, the vault, the key and the policy:
az identity create --name id-bus-cmk --resource-group rg-shop
az keyvault create --name tbshop-kv --resource-group rg-shop \
--enable-purge-protection true --enable-rbac-authorization false
az keyvault key create --vault-name tbshop-kv --name bus-cmk --kty RSA
az keyvault set-policy --name tbshop-kv \
--object-id "$IDENTITY_PRINCIPAL_ID" \
--key-permissions get wrapKey unwrapKey listThen the encryption block names the vault URI, the key and the identity, plus optional infrastructure encryption. Do it at creation: it only works on a new or empty namespace.
resource ns 'Microsoft.ServiceBus/namespaces@2024-01-01' = {
name: 'tbshop-bus'
location: location
sku: { name: 'Premium', tier: 'Premium', capacity: 1 }
identity: {
type: 'UserAssigned'
userAssignedIdentities: { '${uami.id}': {} }
}
properties: {
encryption: {
keySource: 'Microsoft.KeyVault'
keyVaultProperties: [
{
keyName: 'bus-cmk'
keyVaultUri: kv.properties.vaultUri
identity: { userAssignedIdentity: uami.id }
}
]
requireInfrastructureEncryption: true // optional: double
}
}
}Then know how the key behaves in production:
- Polled - every 5 minutes
- Up to 3 keys - one reachable is enough
- All revoked - nothing purged, namespace inoperable until access returns
- Geo-DR - user-assigned identity on both, then pair
Rotate keys before they expire: if every key is revoked or expired, the namespace is inoperable.
Scenario 7: A bank needs a secure, highly available messaging platform across two regions. What do you set up?
Premium, because the bank needs Geo-replication, private endpoints and customer-managed keys. Each region is zone-redundant, and Geo-replication adds the second region, with entities and messages.
Security comes from everything we covered earlier:
- Microsoft Entra ID only -
disableLocalAuth: true - Managed identities - Data Sender / Data Receiver at queue scope
- Private endpoints - one per region, public network access disabled, TLS 1.2+
- Customer-managed key - Key Vault with purge protection
In Bicep: geo data replication with a primary and a secondary location, and a maximum lag for asynchronous mode.
// geoDataReplication is in the stable API 2026-01-01
resource ns 'Microsoft.ServiceBus/namespaces@2026-01-01' = {
name: 'bank-bus'
location: 'eastus2'
sku: { name: 'Premium', tier: 'Premium', capacity: 2 }
properties: {
disableLocalAuth: true // Microsoft Entra ID only
publicNetworkAccess: 'Disabled' // private endpoints only
minimumTlsVersion: '1.2'
geoDataReplication: {
maxReplicationLagDurationInSeconds: 300 // async: max lag
locations: [
{ locationName: 'eastus2', roleType: 'Primary' }
{ locationName: 'centralus', roleType: 'Secondary' }
]
}
// + identity and the encryption block from question six
}
}For payments, I'd argue for synchronous: no acknowledged message is lost, but every send waits for region two, and is throttled if it lags.
Then drill the promotion and run a smoke test after it. Ours sends a probe to the transfers-in queue and the transfers topic, then reads it back from the queue and every subscription. We run python smoke_test.py.
import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
probe = dict(body="smoke test", subject="smoke-test")
checks = [("queue transfers-in", "transfers-in", None)] + [
(f"transfers/{sub}", "transfers", sub)
for sub in ["audit", "fraud", "ledger"]]
with client:
with client.get_queue_sender("transfers-in") as sender:
sender.send_messages(ServiceBusMessage(**probe))
with client.get_topic_sender("transfers") as sender:
sender.send_messages(ServiceBusMessage(**probe))
ok = 0
for name, entity, sub in checks:
rx = (client.get_subscription_receiver(entity, sub) if sub
else client.get_queue_receiver(entity))
with rx:
got = rx.receive_messages(max_wait_time=5)
for msg in got:
rx.complete_message(msg)
good = [m.subject for m in got] == ["smoke-test"]
ok += good
print(f"{name:20}", "ok" if good else "MISSING")
print(f"{ok} of {len(checks)} entities ok")$ python smoke_test.py
queue transfers-in ok
transfers/audit ok
transfers/fraud ok
transfers/ledger ok
4 of 4 entities okThe queue and all three subscriptions print ok, and the result is 4 of 4 entities ok. Real consumers must ignore probes.
Three more things for production:
- Alert on replication lag -
ReplicationLagDuration,ReplicationLagCount - Idempotent consumers - a replay can bring a message twice
- Drill the promotion - then the smoke test
In asynchronous mode, past the maximum lag, the primary throttles incoming requests.
Two regions, no secrets, no public access, your own key, and a drill that proves it works.
Key points to remember
- Microsoft Entra ID first; SAS only where needed, then disable local auth
- Managed identities + Data Sender / Receiver roles at the narrowest scope
- Premium: private endpoint + public access disabled; get DNS right
- Zones on every tier; retries need a stable message ID
- Geo-DR = configuration only; Geo-replication = messages too; consumers idempotent
The full script
This is a Python script that writes every file and runs every command above in order against the Service Bus emulator: start the emulator with SQL Server, run pip install azure-servicebus first, set SERVICEBUS_CONNECTION_STRING, and run it in an empty folder. One step restarts the emulator, and the script says so in a comment.
Show the full script that writes the files and runs every step
# ========================================================================
# Service Bus Interview Q&A #9 - Security, networking and disaster recovery
# TechBrothersIT. Names and sample data are invented for teaching.
# Every step below was run on the official Azure Service Bus emulator (Docker image, version 2.0.1) with
# SQL Server 2022 and azure-servicebus 7.15.0; the outputs in the video and the blog post are the real outputs.
# To run it yourself:
# 1. Start the emulator with Microsoft's installer (github.com/Azure/azure-service-bus-emulator-installer),
# using the Config.json this script writes first (Step 0) as the emulator's config file; it holds every
# queue, topic, subscription, rule and setting (lock duration, max delivery count, duplicate detection...)
# the steps depend on.
# 2. pip install azure-servicebus
# 3. Set SERVICEBUS_CONNECTION_STRING to the emulator connection string (Endpoint=sb://localhost;
# SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;)
# then run python ServiceBus_Interview_QA_09_Code.py.
# Run it in an empty folder and on a freshly started emulator: it creates its files (and any SQLite
# databases) there, and a second run on old files or old messages gives different output.
# Steps that restart the emulator say so in a comment: stop and start it by hand at that point.
# ========================================================================
import os, subprocess, time
def write(path, text):
if text is None:
if os.path.exists(path): os.remove(path)
return
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(text.strip("\n") + "\n")
def run(cmd, expect_error=False, allow_error=False, env=None):
print("\n$ " + cmd, flush=True)
rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
if rc != 0 and not (expect_error or allow_error):
raise SystemExit("stopped: this command was expected to succeed")
# Step 0: the emulator config (mount it as ServiceBus_Emulator/ConfigFiles/Config.json, then start the emulator)
write('Config.json', """\
{
"UserConfig": {
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [
{
"Name": "orders",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
},
{
"Name": "payments",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": true,
"RequiresSession": false
}
},
{
"Name": "orders-dr",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
},
{
"Name": "transfers-in",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
}
],
"Topics": [
{
"Name": "transfers",
"Properties": {
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"RequiresDuplicateDetection": false
},
"Subscriptions": [
{
"Name": "audit",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
},
{
"Name": "fraud",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
},
{
"Name": "ledger",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
}
}
]
}
]
}
],
"Logging": {
"Type": "File"
}
}
}
""")
# Step 1: The parts of a SAS connection string, a SAS token signed locally, and a wrong key on the emulator
write('sas_token.py', 'import base64, hashlib, hmac, os, urllib.parse\n\nconn = os.environ["SERVICEBUS_CONNECTION_STRING"]\nparts = dict(p.split("=", 1) for p in conn.strip(";").split(";"))\nfor name, value in parts.items():\n shown = "*****" if name == "SharedAccessKey" else value\n print(f"{name:22} {shown}")\n\ndef sas_token(uri, key_name, key, expiry):\n sr = urllib.parse.quote_plus(uri)\n to_sign = f"{sr}\\n{expiry}".encode()\n sig = hmac.new(key.encode(), to_sign, hashlib.sha256).digest()\n sig = urllib.parse.quote_plus(base64.b64encode(sig))\n return (f"SharedAccessSignature sr={sr}&sig={sig}"\n f"&se={expiry}&skn={key_name}")\n\ntoken = sas_token("https://tbshop-bus.servicebus.windows.net/orders",\n "orders-send", "SEND_KEY", 1798761600) # 2027-01-01\nfor field in token.split(" ", 1)[1].split("&"):\n print("token", field)\n')
write('wrong_key.py', """\
import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
conn = os.environ["SERVICEBUS_CONNECTION_STRING"]
wrong = conn.replace("SAS_KEY_VALUE", "NOT_THE_REAL_KEY")
with ServiceBusClient.from_connection_string(wrong) as client:
with client.get_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage("order 1001"))
print("wrong key: send accepted (the emulator checks no keys)")
""")
run('python sas_token.py')
run('python wrong_key.py')
# Step 2: A retried send with the same message ID, into a queue with duplicate detection
write('zone_retry.py', """\
import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"],
retry_total=3, retry_backoff_factor=0.8, # the defaults
retry_mode="exponential") # waits 1.6, 3.2, 6.4 s
payment = dict(body="payment 7001", message_id="payment-7001")
with client:
with client.get_queue_sender("payments") as sender:
sender.send_messages(ServiceBusMessage(**payment))
# zone blip: the ack is lost, so the app sends it again
sender.send_messages(ServiceBusMessage(**payment))
with client.get_queue_receiver("payments") as receiver:
got = receiver.receive_messages(max_message_count=10,
max_wait_time=3)
for msg in got:
receiver.complete_message(msg)
print("sent 2 times, received:", [str(m) for m in got])
""")
run('python zone_retry.py')
# Step 3: An idempotent consumer processes four orders (writes keyed by message ID)
write('publish.py', """\
import os, sys
from azure.servicebus import ServiceBusClient, ServiceBusMessage
first, last = int(sys.argv[1]), int(sys.argv[2])
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
with client, client.get_queue_sender("orders-dr") as sender:
sender.send_messages([ServiceBusMessage(f"order {n}",
message_id=f"order-{n}")
for n in range(first, last + 1)])
print(f"published order {first} to order {last}")
""")
write('consume.py', """\
import os, sqlite3
from azure.servicebus import ServiceBusClient
db = sqlite3.connect("shop.db")
db.execute("CREATE TABLE IF NOT EXISTS orders"
" (message_id TEXT PRIMARY KEY, body TEXT)")
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
with client, client.get_queue_receiver("orders-dr",
max_wait_time=3) as receiver:
for msg in receiver:
with db: # one commit per order
new = db.execute("INSERT OR IGNORE INTO orders"
" VALUES (?, ?)",
(msg.message_id, str(msg))).rowcount
print(msg, "processed" if new else "skipped: already done")
receiver.complete_message(msg) # after the commit
total = db.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
print("orders in the database:", total)
""")
run('python publish.py 1001 1004')
run('python consume.py')
# Step 4: Emulator restart as a stand-in for a Geo-DR failover (same queue, no messages): the publisher replays from its outbox
# >>> now RESTART the emulator container (the video restarts it here, keeping SQL Server running) <<<
run('python publish.py 1003 1006')
run('python consume.py')
# Step 5: Smoke test after a promotion: a probe through the queue and every subscription
write('smoke_test.py', """\
import os
from azure.servicebus import ServiceBusClient, ServiceBusMessage
client = ServiceBusClient.from_connection_string(
os.environ["SERVICEBUS_CONNECTION_STRING"])
probe = dict(body="smoke test", subject="smoke-test")
checks = [("queue transfers-in", "transfers-in", None)] + [
(f"transfers/{sub}", "transfers", sub)
for sub in ["audit", "fraud", "ledger"]]
with client:
with client.get_queue_sender("transfers-in") as sender:
sender.send_messages(ServiceBusMessage(**probe))
with client.get_topic_sender("transfers") as sender:
sender.send_messages(ServiceBusMessage(**probe))
ok = 0
for name, entity, sub in checks:
rx = (client.get_subscription_receiver(entity, sub) if sub
else client.get_queue_receiver(entity))
with rx:
got = rx.receive_messages(max_wait_time=5)
for msg in got:
rx.complete_message(msg)
good = [m.subject for m in got] == ["smoke-test"]
ok += good
print(f"{name:20}", "ok" if good else "MISSING")
print(f"{ok} of {len(checks)} entities ok")
""")
run('python smoke_test.py')
Part 10: Real-World Manager Scenarios
The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run on the official Azure Service Bus emulator (2.0.1) with SQL Server and azure-servicebus 7.15.0. Features the emulator does not support are shown as code. Part of the Real Time Azure Service Bus Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of October 2026; check the official Azure Service Bus documentation (learn.microsoft.com/azure/service-bus-messaging) for anything you quote in an interview.



No comments:
Post a Comment
Note: Only a member of this blog may post a comment.