Problem
You use microservice architecture techniques to design a microservice application and have determined that you want to use Azure container apps and Dapr to create these applications. In this article, we will create a microservices architecture example application based on what we learned in Part 1 and Part 2 of this tutorial.
Solution
To complete this task, you will use Bicep to provision resources for your Azure infrastructure and enable the container apps with Dapr as the main resource, depending on running other resources. You will also use Node.js to create the applications to communicate with resources on the data plane.
Prerequisites
- Node.js
- Azure free account
- Visual Studio Code (latest)
- Dapr CLI
Application Layout
The application will be divided into Bicep infrastructure-related code, Bicep application-related code, Yaml Dapr components definitions, and Node.js code for the container apps-injected applications.
The layout will look like this:
- Core (infrastructure Bicep)
- Host (environment)
- Analytics (host analysis and logging)
- Security (infrastructure access control)
- App (application Bicep)
- Main File
- Node.js applications (checkout and order processor)
We will use Bicep to develop the core infrastructure (resources for creating the environment, analyzing the environment, and role management or access control). Then we will create a folder to develop the container apps and the Dapr pub/sub components.
In this tutorial, we will deploy an Azure Container Registry to store different Container apps with Dapr sidecars as images, keeping track of our system using Azure analysis:

Image source: https://learn.microsoft.com/zh-tw/azure/architecture/example-scenario/serverless/microservices-with-container-apps
This tutorial will focus on deploying resources required using Infrastructure as Code (IaC).
Core Infrastructure
In this section, we will identify and learn about the core resources needed to develop our infrastructure. Let’s start with resources that are built independently from other resources and serve as a building block for other resources – this will be our Analytical resources.
Start by creating the main directory for your application, naming it microservice_containerApps_Dapr. Then create a folder named core with three separate folders under it named host, analytics, and security.
Analytics Resources
The analytics folder will house the Bicep code for monitoring your infrastructure and application’s health and performance as part of Azure Monitor to help collect, analyze, and respond to monitoring data from your environments.
Log Analytics Workspace
First, let’s create a new log analytics workspace, i.e., a platform to help us collect, analyze, and debug data from our applications, which is essential to maintaining a good-performing application.
To create a new log analytics workspace, create a Bicep file named log-analytics.bicep and add the following code:
metadata description = 'Creates a Log Analytics workspace.'
param name string = 'log-analytics'
param location string = resourceGroup().location
param tags object = {}
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: name
location: location
tags: tags
properties: any({
retentionInDays: 30
features: {
searchVersion: 1
}
})
}
output id string = logAnalytics.id
output name string = logAnalytics.nameThis file creates a new log analytics workspace and outputs the name and id of the resource.
Application Insight Instance
Now let’s create an application insight instance to enhance your logging capabilities by providing telemetry data such as requests, exceptions, traces, dependencies, custom metrics, and log data. Application Insights is a feature of Azure Monitor that helps with monitoring performance for live web applications, also supports distributed tracing, which allows you to trace requests across services and components. To create an Application Insight resource linked to the log analytics we created above, create a Bicep file named application-insights.bicep and add the following code:
metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.'
param name string
param location string = resourceGroup().location
param tags object = {}
param logAnalyticsWorkspaceId string
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: name
location: location
tags: tags
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspaceId
}
}
output connectionString string = applicationInsights.properties.ConnectionString
output id string = applicationInsights.id
output instrumentationKey string = applicationInsights.properties.InstrumentationKey
output name string = applicationInsights.nameSingle File – Application Insights Instance/Log Analytics Workspace
Now let’s create a single file that will reference both resources created above and help chain the resources together so that the Application Insight can get the linked workspace via its id output. To do this, let’s create a bicep file named monitoring.bicep and add the following code:
metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.'
param logAnalyticsName string
param applicationInsightsName string
param location string = resourceGroup().location
param tags object = {}
module logAnalytics 'log-analytics.bicep' = {
name: 'loganalytics'
params: {
name: logAnalyticsName
location: location
tags: tags
}
}
module applicationInsights 'application-insights.bicep' = {
name: 'applicationinsights'
params: {
name: applicationInsightsName
location: location
tags: tags
logAnalyticsWorkspaceId: logAnalytics.outputs.id
}
}
output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString
output applicationInsightsId string = applicationInsights.outputs.id
output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey
output applicationInsightsName string = applicationInsights.outputs.name
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
output logAnalyticsWorkspaceName string = logAnalytics.outputs.nameHost
The host folder will house the Bicep code for deploying a managed environment, the container apps, updating the container apps, and registry for the container apps.
Azure Container Apps Environment
For the managed environment, create a Bicep file under the host folder named container-apps-env.bicep and add the following code:
metadata description = 'Creates an Azure Container Apps environment.'
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Name of the Application Insights resource')
param applicationInsightsName string = ''
@description('Specifies if Dapr is enabled')
param daprEnabled bool = false
@description('Name of the Log Analytics workspace')
param logAnalyticsWorkspaceName string
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: name
location: location
tags: tags
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : ''
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = {
name: logAnalyticsWorkspaceName
}
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) {
name: applicationInsightsName
}
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output id string = containerAppsEnvironment.id
output name string = containerAppsEnvironment.nameThe code above creates a managed environment to host your container apps which we will define shortly. This resource also enables Dapr and the instrumentation key, which gives access to telemetry data that will be logged by Application Insights.
Azure Container Registry
Our container app will be deployed as docker images and stored on the Azure container registry (ACR). To deploy the ARC, create a Bicep file named container-registry.bicep and add the following code:
metadata description = 'Creates an Azure Container Registry.'
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Indicates whether admin user is enabled')
param adminUserEnabled bool = false
@description('Indicates whether anonymous pull is enabled')
param anonymousPullEnabled bool = false
@description('Azure ad authentication as arm policy settings')
param azureADAuthenticationAsArmPolicy object = {
status: 'enabled'
}
@description('Indicates whether data endpoint is enabled')
param dataEndpointEnabled bool = false
@description('Encryption settings')
param encryption object = {
status: 'disabled'
}
@description('Export policy settings')
param exportPolicy object = {
status: 'enabled'
}
@description('Metadata search settings')
param metadataSearch string = 'Disabled'
@description('Options for bypassing network rules')
param networkRuleBypassOptions string = 'AzureServices'
@description('Public network access setting')
param publicNetworkAccess string = 'Enabled'
@description('Quarantine policy settings')
param quarantinePolicy object = {
status: 'disabled'
}
@description('Retention policy settings')
param retentionPolicy object = {
days: 7
status: 'disabled'
}
@description('Scope maps setting')
param scopeMaps array = []
@description('SKU settings')
param sku object = {
name: 'Basic'
}
@description('Soft delete policy settings')
param softDeletePolicy object = {
retentionDays: 7
status: 'disabled'
}
@description('Trust policy settings')
param trustPolicy object = {
type: 'Notary'
status: 'disabled'
}
@description('Zone redundancy setting')
param zoneRedundancy string = 'Disabled'
@description('The log analytics workspace ID used for logging and monitoring')
param workspaceId string = ''
// 2023-11-01-preview needed for metadataSearch
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
name: name
location: location
tags: tags
sku: sku
properties: {
adminUserEnabled: adminUserEnabled
anonymousPullEnabled: anonymousPullEnabled
dataEndpointEnabled: dataEndpointEnabled
encryption: encryption
metadataSearch: metadataSearch
networkRuleBypassOptions: networkRuleBypassOptions
policies:{
quarantinePolicy: quarantinePolicy
trustPolicy: trustPolicy
retentionPolicy: retentionPolicy
exportPolicy: exportPolicy
azureADAuthenticationAsArmPolicy: azureADAuthenticationAsArmPolicy
softDeletePolicy: softDeletePolicy
}
publicNetworkAccess: publicNetworkAccess
zoneRedundancy: zoneRedundancy
}
resource scopeMap 'scopeMaps' = [for scopeMap in scopeMaps: {
name: scopeMap.name
properties: scopeMap.properties
}]
}
resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {
name: 'registry-diagnostics'
scope: containerRegistry
properties: {
workspaceId: workspaceId
logs: [
{
category: 'ContainerRegistryRepositoryEvents'
enabled: true
}
{
category: 'ContainerRegistryLoginEvents'
enabled: true
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
timeGrain: 'PT1M'
}
]
}
}
output id string = containerRegistry.id
output loginServer string = containerRegistry.properties.loginServer
output name string = containerRegistry.nameContainer Apps
Now to provision your container apps. Create a file named container-app.bicep and add the following code:
metadata description = 'Creates a container app in an Azure Container App environment.'
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Allowed origins')
param allowedOrigins array = []
@description('Name of the environment for container apps')
param containerAppsEnvironmentName string
@description('CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'
@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10
@description('Memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'
@description('The minimum number of replicas to run. Must be at least 1.')
param containerMinReplicas int = 1
@description('The name of the container')
param containerName string = 'main'
@description('The name of the container registry')
param containerRegistryName string = ''
@description('Hostname suffix for container registry. Set when deploying to sovereign clouds')
param containerRegistryHostSuffix string = 'azurecr.io'
@description('The protocol used by Dapr to connect to the app, e.g., http or grpc')
@allowed([ 'http', 'grpc' ])
param daprAppProtocol string = 'http'
@description('The Dapr app ID')
param daprAppId string = containerName
@description('Enable Dapr')
param daprEnabled bool = false
@description('The environment variables for the container')
param env array = []
@description('Specifies if the resource ingress is exposed externally')
param external bool = true
@description('The name of the user-assigned identity')
param identityName string = ''
@description('The type of identity for the resource')
@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])
param identityType string = 'None'
@description('The name of the container image')
param imageName string = ''
@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true
param revisionMode string = 'Single'
@description('The secrets required for the container')
@secure()
param secrets object = {}
@description('The service binds associated with the container')
param serviceBinds array = []
@description('The name of the container apps add-on to use. e.g. redis')
param serviceType string = ''
@description('The target port for the container')
param targetPort int = 80
resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) {
name: identityName
}
// Private registry support requires both an ACR name and a User Assigned managed identity
var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName)
// Automatically set to `UserAssigned` when an `identityName` has been set
var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType
module containerRegistryAccess '../security/container-registry-access.bicep' = if (usePrivateRegistry) {
name: '${deployment().name}-registry-access'
params: {
containerRegistryName: containerRegistryName
principalId: usePrivateRegistry ? userIdentity.properties.principalId : ''
}
}
resource app 'Microsoft.App/containerApps@2023-05-02-preview' = {
name: name
location: location
tags: tags
// It is critical that the identity is granted ACR pull access before the app is created
// otherwise the container app will throw a provision error
// This also forces us to use an user assigned managed identity since there would no way to
// provide the system assigned identity with the ACR pull access before the app is created
dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : []
identity: {
type: normalizedIdentityType
userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null
}
properties: {
managedEnvironmentId: containerAppsEnvironment.id
configuration: {
activeRevisionsMode: revisionMode
ingress: ingressEnabled ? {
external: external
targetPort: targetPort
transport: 'auto'
corsPolicy: {
allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
}
} : null
dapr: daprEnabled ? {
enabled: true
appId: daprAppId
appProtocol: daprAppProtocol
appPort: ingressEnabled ? targetPort : 0
} : { enabled: false }
secrets: [for secret in items(secrets): {
name: secret.key
value: secret.value
}]
service: !empty(serviceType) ? { type: serviceType } : null
registries: usePrivateRegistry ? [
{
server: '${containerRegistryName}.${containerRegistryHostSuffix}'
identity: userIdentity.id
}
] : []
}
template: {
serviceBinds: !empty(serviceBinds) ? serviceBinds : null
containers: [
{
image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
name: containerName
env: env
resources: {
cpu: json(containerCpuCoreCount)
memory: containerMemory
}
}
]
scale: {
minReplicas: containerMinReplicas
maxReplicas: containerMaxReplicas
}
}
}
}
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = {
name: containerAppsEnvironmentName
}
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId)
output imageName string = imageName
output name string = app.name
output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}
output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : ''The code above creates a container app with Dapr enabled, along with its container registry access to control user access for the container apps.
You can deploy your resources as much as you can, and if you deploy a duplicate, Azure will not deploy the duplicate deployment, but won’t give you an error. It is ideal to create a file that enables you to update your container apps after they are deployed. Create a bicep file named container-app-upsert.bicep and add the following code:
metadata description = 'Creates or updates an existing Azure Container App.'
param name string
param location string = resourceGroup().location
param tags object = {}
@description('The environment name for the container apps')
param containerAppsEnvironmentName string
@description('The number of CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'
@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10
@description('The amount of memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'
@description('The minimum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMinReplicas int = 1
@description('The name of the container')
param containerName string = 'main'
@description('The name of the container registry')
param containerRegistryName string = ''
@description('Hostname suffix for container registry. Set when deploying to sovereign clouds')
param containerRegistryHostSuffix string = 'azurecr.io'
@allowed([ 'http', 'grpc' ])
@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC')
param daprAppProtocol string = 'http'
@description('Enable or disable Dapr for the container app')
param daprEnabled bool = false
@description('The Dapr app ID')
param daprAppId string = containerName
@description('Specifies if the resource already exists')
param exists bool = false
@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true
@description('The type of identity for the resource')
@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])
param identityType string = 'None'
@description('The name of the user-assigned identity')
param identityName string = ''
@description('The name of the container image')
param imageName string = ''
@description('The secrets required for the container')
@secure()
param secrets object = {}
@description('The environment variables for the container')
param env array = []
@description('Specifies if the resource ingress is exposed externally')
param external bool = true
@description('The service binds associated with the container')
param serviceBinds array = []
@description('The target port for the container')
param targetPort int = 80
resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) {
name: name
}
module app 'container-app.bicep' = {
name: '${deployment().name}-update'
params: {
name: name
location: location
tags: tags
identityType: identityType
identityName: identityName
ingressEnabled: ingressEnabled
containerName: containerName
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerRegistryHostSuffix: containerRegistryHostSuffix
containerCpuCoreCount: containerCpuCoreCount
containerMemory: containerMemory
containerMinReplicas: containerMinReplicas
containerMaxReplicas: containerMaxReplicas
daprEnabled: daprEnabled
daprAppId: daprAppId
daprAppProtocol: daprAppProtocol
secrets: secrets
external: external
env: env
imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''
targetPort: targetPort
serviceBinds: serviceBinds
}
}
output defaultDomain string = app.outputs.defaultDomain
output imageName string = app.outputs.imageName
output name string = app.outputs.name
output uri string = app.outputs.uriACR/Azure Container Apps Environment
Now onto bundling the container apps with their respective container app registry. Create a Bicep file named container-apps.bicep and add the following code:
metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.'
param name string
param location string = resourceGroup().location
param tags object = {}
param containerAppsEnvironmentName string
param containerRegistryName string
param containerRegistryResourceGroupName string = ''
param containerRegistryAdminUserEnabled bool = false
param logAnalyticsWorkspaceName string
param applicationInsightsName string = ''
param daprEnabled bool = false
module containerAppsEnvironment 'container-apps-env.bicep' = {
name: '${name}-container-apps-environment'
params: {
name: containerAppsEnvironmentName
location: location
tags: tags
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
applicationInsightsName: applicationInsightsName
daprEnabled: daprEnabled
}
}
module containerRegistry 'container-registry.bicep' = {
name: '${name}-container-registry'
scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup()
params: {
name: containerRegistryName
location: location
adminUserEnabled: containerRegistryAdminUserEnabled
tags: tags
}
}
output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain
output environmentName string = containerAppsEnvironment.outputs.name
output environmentId string = containerAppsEnvironment.outputs.id
output registryLoginServer string = containerRegistry.outputs.loginServer
output registryName string = containerRegistry.outputs.nameSecurity
On the security folder, you will assign a role for a user to access the ACR by creating a file named container-registry-access.bicep and adding the following code:
targetScope = 'resourceGroup'
metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.'
param containerRegistryName string
param principalId string
var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = {
name: containerRegistryName
}
resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: containerRegistry // Use when specifying a scope that is different than the deployment scope
name: guid(resourceGroup().id, containerRegistryName, principalId, acrPullRole)
properties: {
roleDefinitionId: acrPullRole
principalType: 'ServicePrincipal'
principalId: principalId
}
}That should cover the core folder. Below is what the folder structure should look like after adding the files:
- Core (infrastructure Bicep)
- Host (environment)
- Container-apps-env.bicep
- Container-app.bicep
- Container-registry.bicep
- Container-apps.bicep
- Container-apps-upsert.bicep
- Analytics (host analysis and logging)
- Log-analytics.bicep
- Application-insights.bicep
- Monitoring.bicep
- Security (infrastructure access control)
- container-registry-access.bicep
- Host (environment)
Next, we move on to the files for the App folder.
App Folder
Let’s start with a file that will inherit the container apps resources created in the previous section as a module and chain it to a Dapr sup/pub component. Create a Bicep file named app-env.bicep and add the following code:
param containerAppsEnvName string
param containerRegistryName string
param location string
param logAnalyticsWorkspaceName string
param serviceBusName string
param applicationInsightsName string = ''
param daprEnabled bool = false
param managedIdentityClientId string
// Container apps host (including container registry)
module containerApps '../core/host/container-apps.bicep' = {
name: 'container-apps'
params: {
name: 'apps'
containerAppsEnvironmentName: containerAppsEnvName
containerRegistryName: containerRegistryName
location: location
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
applicationInsightsName: applicationInsightsName
daprEnabled: daprEnabled
}
}
// Get cApps Env resource instance to parent Dapr component config under it
resource caEnvironment 'Microsoft.App/managedEnvironments@2022-06-01-preview' existing = {
name: containerAppsEnvName
}
resource daprComponentPubsub 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
parent: caEnvironment
name: 'orderpubsub'
properties: {
componentType: 'pubsub.azure.servicebus'
version: 'v1'
metadata: [
{
name: 'azureClientId'
value: managedIdentityClientId // See DefaultAzureCredentialOptions.ManagedIdentityClientId Property (Azure.Identity) - Azure for .NET Developers | Microsoft Learn for MSI
}
{
name: 'namespaceName' // See Azure Service Bus Topics | Dapr Docs
value: '${serviceBusName}.servicebus.windows.net' // the .servicebus.windows.net suffix is required as per dapr docs
}
{
name: 'consumerID'
value: 'orders' // Set to the same value of the subscription seen in ./servicebus.bicep
}
]
scopes: []
}
dependsOn: [
containerApps
]
}
output environmentName string = containerApps.outputs.environmentName
output registryLoginServer string = containerApps.outputs.registryLoginServer
output registryName string = containerApps.outputs.registryNameThe code above refers to the Bicep created earlier to provision a container app with a managed environment and ACR. The code then adds a Dapr sub/pub component resource with a property value componentType: ‘pubsub.azure.servicebus’
Then, to create a service bus resource, create a Bicep file named servicebus.bicep and add the following code:
param resourceToken string
param location string
param skuName string = 'Standard'
param topicName string = 'orders'
param tags object
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
name: 'sb-${resourceToken}'
location: location
tags: tags
sku: {
name: skuName
tier: skuName
}
properties: {
minimumTlsVersion: '1.2'
}
resource topic 'topics' = {
name: topicName
properties: {
supportOrdering: true
}
resource subscription 'subscriptions' = {
name: topicName
properties: {
deadLetteringOnFilterEvaluationExceptions: true
deadLetteringOnMessageExpiration: true
maxDeliveryCount: 10
}
}
}
}
output SERVICEBUS_ENDPOINT string = serviceBusNamespace.properties.serviceBusEndpoint
output serviceBusName string = serviceBusNamespace.nameTo control the service bus access, create a Bicep file named access-control.bicep and add the following code:
param managedIdentityName string
param serviceBusName string
param location string
// See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender
var roleIdS = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' // Azure Service Bus Data Sender
// See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-receiver
var roleIdR = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' // Azure Service Bus Data Receiver
// user assigned managed identity to use throughout
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: managedIdentityName
location: location
}
resource serviceBus 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = {
name: serviceBusName
}
// Grant permissions to the managedIdentity to specific role to servicebus
resource roleAssignmentReceiver 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(serviceBus.id, roleIdR, managedIdentityName)
scope: serviceBus
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdR)
principalId: managedIdentity.properties.principalId
principalType: 'ServicePrincipal' // managed identity is a form of service principal
}
dependsOn: [
serviceBus
]
}
// Grant permissions to the managedIdentity to specific role to servicebus
resource roleAssignmentSender 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(serviceBus.id, roleIdS, managedIdentityName)
scope: serviceBus
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdS)
principalId: managedIdentity.properties.principalId
principalType: 'ServicePrincipal' // managed identity is a form of service principal
}
dependsOn: [
serviceBus
]
}
output managedIdentityPrincipalId string = managedIdentity.properties.principalId
output managedIdentityClientlId string = managedIdentity.properties.clientId
output managedIdentityId string = managedIdentity.id
output managedIdentityName string = managedIdentity.nameThen, let’s create a Bicep file to provision your publisher container apps. These will be container apps to host the publisher microservices. Create a file named publisher.bicep and add the following code:
param location string = resourceGroup().location
param tags object = {}
param containerAppsEnvironmentName string
param containerRegistryName string
param name string = ''
param serviceName string = 'checkout'
param managedIdentityName string = ''
param exists bool = false
module publisher '../core/host/container-app-upsert.bicep' = {
name: '${serviceName}-container-app-module'
params: {
name: name
location: location
tags: union(tags, { 'azd-service-name': 'checkout' })
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerCpuCoreCount: '1.0'
containerMemory: '2.0Gi'
daprEnabled: true
containerName: serviceName
daprAppId: serviceName
ingressEnabled: false
identityType: 'UserAssigned'
identityName: managedIdentityName
exists: exists
}
}
output SERVICE_PUBLISHER_IMAGE_NAME string = publisher.outputs.imageName
output SERVICE_PUBLISHER_NAME string = publisher.outputs.nameFor subscriber microservices, create a Bicep file named subscriber.bicep and add the following code:
param location string = resourceGroup().location
param tags object = {}
param containerAppsEnvironmentName string
param containerRegistryName string
param name string = ''
param serviceName string = 'orders'
param managedIdentityName string = ''
param exists bool = false
module subscriber '../core/host/container-app-upsert.bicep' = {
name: '${serviceName}-container-app-module'
params: {
name: name
location: location
tags: union(tags, { 'azd-service-name': 'orders' })
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerCpuCoreCount: '1.0'
containerMemory: '2.0Gi'
exists: exists
daprEnabled: true
containerName: serviceName
daprAppId: serviceName
targetPort: 5001
identityType: 'UserAssigned'
identityName: managedIdentityName
}
}
output SUBSCRIBER_URI string = subscriber.outputs.uri
output SERVICE_SUBSCRIBER_IMAGE_NAME string = subscriber.outputs.imageName
output SERVICE_SUBSCRIBER_NAME string = subscriber.outputs.nameThe App folder should look like this after adding bicep files:
- App
- App-env.bicep
- Access.bicep
- Servicebus.bicep
- Publisher.bicep
- Subscriber.bicep
The Main File
Now, it is time to bring the entire project together. In the main directory, create a Bicep file named main.bicep and add the following code:
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
@description('Name of the environment which is used to generate a short unique hash used in all resources.')
param environmentName string
@minLength(1)
@description('Primary location for all resources')
param location string
// App based params
// Publisher
param publisherContainerAppName string = ''
param publisherServiceName string = 'checkout'
param publisherAppExists bool = false
//Subscriber
param subscriberContainerAppName string = ''
param subscriberServiceName string = 'orders'
param subscriberAppExists bool = false
param applicationInsightsName string = ''
param logAnalyticsName string = ''
param containerAppsEnvironmentName string = ''
param containerRegistryName string = ''
param resourceGroupName string = ''
// Optional parameters to override the default azd resource naming conventions. Update the main.parameters.json file to provide values. e.g.,:
// "resourceGroupName": {
// "value": "myGroupName"
// }
var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
var tags = { 'azd-env-name': environmentName }
// Organize resources in a resource group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
location: location
tags: tags
}
// Monitor application with Azure Monitor
module monitoring './core/analytics/monitoring.bicep' = {
name: 'monitoring'
scope: rg
params: {
location: location
tags: tags
logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}'
applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}'
}
}
module serviceBusResources './app/servicebus.bicep' = {
name: 'sb-resources'
scope: rg
params: {
location: location
tags: tags
resourceToken: resourceToken
skuName: 'standard'
}
}
module serviceBusAccess './app/access.bicep' = {
name: 'sb-access'
scope: rg
params: {
location: location
serviceBusName: serviceBusResources.outputs.serviceBusName
managedIdentityName: '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}'
}
}
// Shared App Env with Dapr configuration for db
module appEnv './app/app-env.bicep' = {
name: 'app-env'
scope: rg
params: {
containerAppsEnvName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}'
containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}'
location: location
logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName
serviceBusName: serviceBusResources.outputs.serviceBusName
applicationInsightsName: monitoring.outputs.applicationInsightsName
daprEnabled: true
managedIdentityClientId: serviceBusAccess.outputs.managedIdentityClientlId
}
}
module publisherApp './app/publisher.bicep' = {
name: 'api-resources'
scope: rg
params: {
name: !empty(publisherContainerAppName) ? publisherContainerAppName : '${abbrs.appContainerApps}${publisherServiceName}-${resourceToken}'
serviceName: publisherServiceName
containerRegistryName: appEnv.outputs.registryName
location: location
containerAppsEnvironmentName: appEnv.outputs.environmentName
managedIdentityName: serviceBusAccess.outputs.managedIdentityName
exists: publisherAppExists
}
dependsOn: [
subscriberApp // Deploy the subscriber first and then deploy the publisher
]
}
module subscriberApp './app/subscriber.bicep' = {
name: 'web-resources'
scope: rg
params: {
name: !empty(subscriberContainerAppName) ? subscriberContainerAppName : '${abbrs.appContainerApps}${subscriberServiceName}-${resourceToken}'
location: location
containerRegistryName: appEnv.outputs.registryName
containerAppsEnvironmentName: appEnv.outputs.environmentName
serviceName: subscriberServiceName
managedIdentityName: serviceBusAccess.outputs.managedIdentityName
exists: subscriberAppExists
}
}
output SERVICE_PUBLISHER_NAME string = publisherApp.outputs.SERVICE_PUBLISHER_NAME
output SERVICE_PUBLISHER_IMAGE_NAME string = publisherApp.outputs.SERVICE_PUBLISHER_IMAGE_NAME
output SERVICE_SUBSCRIBER_NAME string = subscriberApp.outputs.SERVICE_SUBSCRIBER_NAME
output SERVICE_SUBSCRIBER_IMAGE_NAME string = subscriberApp.outputs.SERVICE_SUBSCRIBER_IMAGE_NAME
output SUBSCRIBER_APP_URI string = subscriberApp.outputs.SUBSCRIBER_URI
output SERVICEBUS_ENDPOINT string = serviceBusResources.outputs.SERVICEBUS_ENDPOINT
output SERVICEBUS_NAME string = serviceBusResources.outputs.serviceBusName
output APPINSIGHTS_INSTRUMENTATIONKEY string = monitoring.outputs.applicationInsightsInstrumentationKey
output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName
output AZURE_CONTAINER_ENVIRONMENT_NAME string = appEnv.outputs.environmentName
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = appEnv.outputs.registryLoginServer
output AZURE_CONTAINER_REGISTRY_NAME string = appEnv.outputs.registryName
output AZURE_MANAGED_IDENTITY_NAME string = serviceBusAccess.outputs.managedIdentityNameBefore deploying your resources, you must create a JSON file with your parameters to avoid providing the parameters on your Azure CLI deployment command. Create a new JSON file named parameters.JSON and add the following code:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "AzureDaprEnv"
},
"location": {
"value": "eastus"
},
"publisherAppExists": {
"value": false
},
"subscriberAppExists": {
"value": false
}
}
}Deploy the Infrastructure
To deploy your application, use this Azure command:
az deployment group create --name myDeployment --resource-group myResourceGroup --template-file main.bicep --parameters main.parameters.jsonAfter running the command, you will see a success message on your terminal. However, you can get a clearer picture of the deployed resources by logging into the Azure Portal and opening the resource group you use:

Node.js Projects
Now, create a folder for your Node.js projects. These projects communicate and interact with your deployed resources on a data plane. Before continuing, make sure to install the Dapr client for node.js:
npm i dapr-clientThen, create a folder for the publisher application named checkout, add a file named index.js, and add the following code:
import { DaprClient } from 'dapr-client';
const DAPR_HOST = process.env.DAPR_HOST || "http://localhost";
const DAPR_HTTP_PORT = process.env.DAPR_HTTP_PORT || "3500";
const PUBSUB_NAME = "orderpubsub";
const PUBSUB_TOPIC = "orders";
async function main() {
const client = new DaprClient(DAPR_HOST, DAPR_HTTP_PORT);
while (true) {
for (var i = 1; i <= 20; i++) {
const order = {
orderId: i
};
// Publish an event using Dapr pub/sub
await client.pubsub.publish(PUBSUB_NAME, PUBSUB_TOPIC, order);
console.log("Published data: " + JSON.stringify(order));
await sleep(1000);
}
await sleep(20000);
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main().catch(e => console.error(e))Create a JSON file named package.json and add the following code:
{
"name": "checkout",
"version": "1.0.0",
"description": "Checkout service for pub/sub with Dapr on Azure",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dapr-client": "^2.3.0"
}
}Create a new Docker file to connect the Node.js project to the container apps image in the ACR:
# Use the Node.js base image from Alpine Linux
FROM node:lts-alpine
# Set the environment to production
ENV NODE_ENV=production
# Define the working directory inside the container
# You should point this to the relevant path where your application code will reside inside the container.
WORKDIR /usr/src/app
# Copy your package.json and lock files first to install dependencies
COPY ["package.json", "./"]
# Install only production dependencies
RUN npm install --production --silent && mv node_modules ../
# Copy the rest of your application code into the container
# This will copy everything from the directory where Dockerfile is located
COPY . .
# Expose the application on port 5001
EXPOSE 5001
EXPOSE 3500
# Change ownership to node user
RUN chown -R node /usr/src/app
# Switch to node user
USER node
# Command to run your application
CMD ["npm", "start"]Create another Node.js project named order-processor, add a file named index.js, and add the following code:
import { DaprServer } from 'dapr-client';
const DAPR_HOST = process.env.DAPR_HOST || "http://localhost:9411";
const DAPR_HTTP_PORT = process.env.DAPR_HTTP_PORT || "3500";
const SERVER_HOST = process.env.SERVER_HOST || "127.0.0.1";
const SERVER_PORT = process.env.APP_PORT || 3000;
async function main() {
const server = new DaprServer(SERVER_HOST, SERVER_PORT, DAPR_HOST, DAPR_HTTP_PORT);
// Dapr subscription routes orders topic to this route
server.pubsub.subscribe("orderpubsub", "orders", (data) => console.log("Subscriber received: " + JSON.stringify(data)));
await server.start();
}
main().catch(e => console.error(e));Create a JSON file named package.json and add the following code:
{
"name": "checkout",
"version": "1.0.0",
"description": "order processor service for pub/sub with Dapr on Azure",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dapr-client": "^2.3.0"
}
}Create another docker file for the subscription application:
# Use the Node.js base image from Alpine Linux
FROM node:lts-alpine
# Set the environment to production
ENV NODE_ENV=production
# Define the working directory inside the container
# You should point this to the relevant path where your application code will reside inside the container.
WORKDIR /usr/src/app
# Copy your package.json and lock files first to install dependencies
COPY ["package.json", "./"]
# Install only production dependencies
RUN npm install --production --silent && mv node_modules ../
# Copy the rest of your application code into the container
# This will copy everything from the directory where Dockerfile is located
COPY . .
# Expose the application on port 5001
EXPOSE 5001
EXPOSE 3500
# Change ownership to node user
RUN chown -R node /usr/src/app
# Switch to node user
USER node
# Command to run your application
CMD ["npm", "start"]Run the Node.js Projects
To begin this section, you must ensure that the Node.js Dapr client is installed:
npm install dapr-clientAfter successfully deploying your infrastructure, you can run the Node.js applications on the infrastructure by injecting the Node.js projects into the container apps created earlier. You must create an authorization rule and connection string to the topic created on Dapr. We will use the resource name, namespace, and topics to create the authorization rule for the topics and the connection string. These values are given when you deploy your infrastructure as an output on your terminal, or you can get these values via the Azure Portal:

To get the values for the connection string using the following commands, first you must create an authorization rule for the “orders” service bus topic created in the previous tutorials:
az servicebus topic authorization-rule create --resource-group <resource-group-name> --namespace-name <namespace-name> --topic-name <topic name> --name <auth rule name> --rights Manage Send ListenThis will give you an output on your terminal:

Then, create the connection string using the following command:
az webapp config connection-string set -g MyResourceGroup -n MyUniqueApp -t ServiceBus –s MySlotName –settings servicebus=’<MyServiceBusIdEndpoint>’Next, create a Yaml file to establish the connection for the Dapr commands against Azure by creating a file named servicebus.yalm and adding the following code:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: orderpubsub # This should match the pubsub name in your app (e.g., 'orderpubsub')
namespace: default # Replace with your namespace (or leave blank for default)
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
value: "<YourConnectionString>"Now, to create the component that establishes communication between the checkout sidecars and the Azure service bus, you can use this az command:
az containerapp update --name ca-checkout-fdrtujfacex4o --resource-group rg-AzureDaprEnv --set dapr.components='[{"name": "orderpubsub", "type": "pubsub.azure.servicebus", "metadata": [{"name": "connectionString", "value": "<YourConnectionString"}]}]'For the orders component, use the following az command:
az containerapp update --name ca-orders-fdrtujfacex4o --resource-group rg-AzureDaprEnv --set dapr.components='[{"name": "orderpubsub", "type": "pubsub.azure.servicebus", "metadata": [{"name": "connectionString", "value": "<YourConnectionString>"}]}]'Then navigate to your Node.js publisher project and use this Dapr command to run the application:
dapr run --app-port 5001 --app-id <publisher_app_id> --app-protocol http --dapr-http-port 3501 --components-path ../components -- npm run startLast, navigate to your Node.js subscriber project and use this Dapr command to run the application:
dapr run --app-port 5001 --app-id <subscriber_app_id> --app-protocol http --dapr-http-port 3501 --components-path ../components -- npm run startConclusion
Great job for going all the way! This tutorial aims to take you through the process of deploying Azure infrastructure resources, integrating your Azure resources with Dapr, and creating Node.js applications to be injected into Azure container apps, which uses the pub/sub technique to distribute communication between your applications. This tutorial also demonstrates how to manually deploy Bicep files. You can deploy and update the Bicep templates using Azure Pipelines, which will be covered in Part 4 of this series.
Next Steps
- Learn more about Serverless Microservice Computing Concepts and Architecture
- Learn how to Plan Infrastructure for a SQL Server Data Warehouse
- Compare Azure Analysis Services with On-Premises SQL Server Analysis Services
- Learn how to Build Azure Data Factory Pipelines with On-Premises Data Sources

Levi Masonde is a developer passionate about analyzing large datasets and creating useful information from these data. He is proficient in Python, ReactJS, and Power Platform applications. He is responsible for creating applications and managing databases as well as a lifetime student of programming and enjoys learning new technologies and how to utilize and share what he learns.
- MSSQLTips Awards:
Trendsetter (25+ tips) – 2025 | Author of the Year Contender – 2023 | Rookie of the Year Contender – 2022



This is amazing, thanks for sharing