Skip to content

Build the Infrastructure as Code Using Bicep

Module Duration

30 minutes

Note

If you're not interested in manually deploying the Bicep files or creating the container registry yourself, and prefer not to delve into the details of how they work, then you can skip this section and head directly to either Deploy Infrastructure Using Github Actions or Deploy Infrastructure Using Azure DevOps depending on your DevOps tool of choice.

To begin, we need to define the Bicep modules that will be required to generate the Infrastructure code. Our goal for this module is to have a freshly created resource group that encompasses all the necessary resources and configurations - such as connection strings, secrets, environment variables, and Dapr components - which we utilized to construct our solution. By the end, we will have a new resource group that includes the following resources.

aca-resources

Note

To simplify the execution of the module, we will assume that you have already created latest images of three services and pushed them to a container registry. This section below guides you through different options of getting images pushed to either Azure Container Registry (ACR) or GitHub Container Registry (GHCR).

1. Add the Needed Extension to VS Code

To proceed, you must install an extension called Bicep. This extension will simplify building Bicep files as it offers IntelliSense, Validation, listing all available resource types, etc..

2. Define an Azure Container Apps Environment

Add a new folder named bicep on the root project directory, then add another folder named modules. Add file as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------
@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of the container apps environment. If set, it overrides the name generated by the template.')
param containerAppsEnvironmentName string 

@description('The name of the log analytics workspace. If set, it overrides the name generated by the template.')
param logAnalyticsWorkspaceName string 

@description(' The name of the application insights. If set, it overrides the name generated by the template.')
param applicationInsightName string 


// ------------------
// RESOURCES
// ------------------
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
  name: logAnalyticsWorkspaceName
  location: location
  tags: tags
  properties: any({
    features: {
      searchVersion: 1
    }
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  })
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightName
  location: location
  tags: tags
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspace.id
  }
}

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-10-01' = {
  name: containerAppsEnvironmentName
  location: location
  tags: tags
  sku: {
    name: 'Consumption'
  }
  properties: {
    daprAIInstrumentationKey: applicationInsights.properties.InstrumentationKey
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalyticsWorkspace.properties.customerId
        sharedKey:  logAnalyticsWorkspace.listKeys().primarySharedKey
      }
    }
  }
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the application insights.')
output applicationInsightsName string  = applicationInsights.name
What we've added in the Bicep file above
  • The module takes multiple parameters, all of which are set to default values. This indicates that if no value is specified, the default value will be utilized.
  • The location parameter defaults to the location of the container resource group. Bicep has a function called resourceGroup(), which can be used to retrieve the location.
  • The parameters prefix and suffix could be used if you want to add a prefix or suffix to the resource names.
  • The parameter tag is used to tag the created resources. Tags are key-value pairs that help you identify resources based on settings that are relevant to your organization and deployment.
  • The parameters containerAppsEnvironmentName, logAnalyticsWorkspaceName, and applicationInsightName have default values of resource names using the helper function named uniqueString. This function performs a 64-bit hash of the provided strings to create a unique string. This function is helpful when you need to create a unique name for a resource. We are passing the resourceGroup().id to this function to ensure that if we executed this module on two different resource groups, the generated string will be a global unique name.
  • This module will create two resources. It will start by creating a logAnalyticsWorkspace, then an applicationInsights resource. Notice how we are setting the logAnalyticsWorkspace.id as an Application Insights WorkspaceResourceId.
  • Lastly we are creating the containerAppsEnvironment. Notice how we are setting the daprAIInstrumentationKey by using the Application Insights InstrumentationKey and then setting logAnalyticsConfiguration.customerId and logAnalyticsConfiguration.sharedKey.
  • The output of this module are a is parameter named applicationInsightsName. This output is needed as an input for a subsequent module.

3. Define an Azure Key Vault Resource

Add file as shown below under the folder bicep\modules:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('The name of the Key Vault. If set, it overrides the name generated by the template.')
param KEYVAULT_NAME string

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

// ------------------
// RESOURCES
// ------------------

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  name: KEYVAULT_NAME
  location: location  
  tags: tags
  properties: {
    tenantId: subscription().tenantId
    sku: {
      family: 'A'
      name: 'standard'
    }
    enableSoftDelete: false
    softDeleteRetentionInDays: 7
    enablePurgeProtection: null  // It seems that you cannot set it to False even the first time. workaround is not to set it at all: https://github.com/Azure/bicep/issues/5223
    enableRbacAuthorization: true
    enabledForTemplateDeployment: true
  }
}

// ------------------
// OUTPUTS
// ------------------

@description('The resource ID of the key vault.')
output keyVaultId string = keyVault.id
What we've added in the Bicep file above
  • This module will create the Azure Key Vault resource which will be used to store secrets.
  • The output of this module is a single parameter named keyVaultId. This output is needed as an input for a subsequent module.

4. Define a Azure Service Bus Resource

Add file as shown below under the folder bicep\modules:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The name of the service bus namespace. If set, it overrides the name generated by the template.')
param serviceBusName string

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of the service bus topic.')
param serviceBusTopicName string

@description('The name of the service bus topic\'s authorization rule.')
param serviceBusTopicAuthorizationRuleName string

@description('The name of the service for the backend processor service. The name is used as Dapr App ID and as the name of service bus topic subscription.')
param backendProcessorServiceName string

// ------------------
// RESOURCES
// ------------------

resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' = {
  name: serviceBusName
  location: location
  tags: tags
  sku: {
    name: 'Standard'
  }
}

resource serviceBusTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = {
  name: serviceBusTopicName
  parent: serviceBusNamespace
}

resource serviceBusTopicAuthRule 'Microsoft.ServiceBus/namespaces/topics/authorizationRules@2021-11-01' = {
  name: serviceBusTopicAuthorizationRuleName
  parent: serviceBusTopic
  properties: {
    rights: [
      'Manage'
      'Send'
      'Listen'
    ]
  }
}

resource serviceBusTopicSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
  name: backendProcessorServiceName
  parent: serviceBusTopic
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the service bus namespace.')
output serviceBusName string = serviceBusNamespace.name

@description('The name of the service bus topic.')
output serviceBusTopicName string = serviceBusTopic.name

@description('The name of the service bus topic\'s authorization rule.')
output serviceBusTopicAuthorizationRuleName string = serviceBusTopicAuthRule.name
What we've added in the Bicep file above
  • This module will create the Azure Service resource, a topic, a subscription for the consumer, and an authorization rule with Manage permissions.
  • The output of this module will return three output parameters which will be used as an input for a subsequent module.

5. Define an Azure CosmosDb Resource

Add file as shown below under the folder bicep\modules:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of Cosmos DB resource.')
param cosmosDbName string

@description('The name of Cosmos DB\'s database.')
param cosmosDbDatabaseName string

@description('The name of Cosmos DB\'s collection.')
param cosmosDbCollectionName string

// ------------------
// RESOURCES
// ------------------
resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = {
  name: cosmosDbName
  location: location
  tags: tags
  kind: 'GlobalDocumentDB'
  properties: {
    locations: [
      {
        locationName: location
        failoverPriority: 0
        isZoneRedundant: false
      }
    ]
    databaseAccountOfferType: 'Standard'
    publicNetworkAccess:'Enabled'
  }
}

resource cosmosDbDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-04-15' = {
  name: cosmosDbDatabaseName
  parent: cosmosDbAccount
  tags: tags
  properties: {
    resource: {
      id: cosmosDbDatabaseName
    }
  }
}

resource cosmosDbDatabaseCollection 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-05-15' = {
  name: cosmosDbCollectionName
  parent: cosmosDbDatabase
  tags: tags
  properties: {
    resource: {
      id: cosmosDbCollectionName
      partitionKey: {
        paths: [
          '/partitionKey'
        ]
        kind: 'Hash'
      }
    }
    options: {
      autoscaleSettings: {
        maxThroughput: 4000
      }
    }
  }
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of Cosmos DB resource.')
output cosmosDbName string = cosmosDbAccount.name
@description('The name of Cosmos DB\'s database.')
output cosmosDbDatabaseName string = cosmosDbDatabase.name
@description('The name of Cosmos DB\'s collection.')
output cosmosDbCollectionName string = cosmosDbDatabaseCollection.name
What we've added in the Bicep file above
  • This module will create the Azure Cosmos DB account, a Cosmos DB database, and a Cosmos DB collection.
  • The output of this module will return three output parameters which will be used as an input for a subsequent module.

6. Define an Azure Storage Resource

Add file as shown below under the folder bicep\modules:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of the external Azure Storage Account.')
param storageAccountName string

@description('The name of the external Queue in Azure Storage.')
param externalTasksQueueName string

// ------------------
// RESOURCES
// ------------------

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: storageAccountName
  tags: tags
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

resource storageQueuesService 'Microsoft.Storage/storageAccounts/queueServices@2021-09-01' = {
  name: 'default'
  parent: storageAccount
}

resource externalQueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2021-09-01' = {
  name: externalTasksQueueName
  parent: storageQueuesService
}

// ------------------
// OUTPUTS
// ------------------

@description('The external storage account name.')
output storageAccountName string = storageAccount.name
What we've added in the Bicep file above
  • This module will create the Azure Storage account, a storage queue service, and a queue.
  • The output of this module will be a single output parameter which will be used as an input for a subsequent module.

7. Define Dapr Components

Next we will define all dapr components used in the solution in a single bicep module. To accomplish this, add a new file under the folder bicep\modules as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The name of the container apps environment.')
param containerAppsEnvironmentName string

@description('The name of Dapr component for the secret store building block.')
// We disable lint of this line as it is not a secret but the name of the Dapr component
#disable-next-line secure-secrets-in-params
param secretStoreComponentName string

@description('The name of the key vault resource.')
param KEYVAULT_NAME string

@description('The name of the service bus namespace.')
param serviceBusName string

@description('The name of Cosmos DB resource.')
param cosmosDbName string

@description('The name of Cosmos DB\'s database.')
param cosmosDbDatabaseName string

@description('The name of Cosmos DB\'s collection.')
param cosmosDbCollectionName string

@description('The name of the external Azure Storage Account.')
param storageAccountName string

@description('The name of the external Queue in Azure Storage.')
param externalTasksQueueName string

@description('The name of the external blob container in Azure Storage.')
param externalTasksContainerBlobName string

@description('The name of the secret containing the External Azure Storage Access key.')
param externalStorageKeySecretName string

@description('The name of the Send Grid Email From.')
param sendGridEmailFrom string

@description('The name of the Send Grid Email From Name.')
param sendGridEmailFromName string

@description('The name of the secret containing the SendGrid API key value.')
param sendGridKeySecretName string

@description('The cron settings for scheduled job.')
param scheduledJobCron string 

@description('The name of the service for the backend api service. The name is used as Dapr App ID.')
param backendApiServiceName string

@description('The name of the service for the backend processor service. The name is used as Dapr App ID and as the name of service bus topic subscription.')
param backendProcessorServiceName string

// ------------------
// RESOURCES
// ------------------

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
  name: containerAppsEnvironmentName
}

resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
  name: cosmosDbName
}

//Secret Store Component
resource secretstoreComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-03-01' = {
  name: secretStoreComponentName
  parent: containerAppsEnvironment
  properties: {
    componentType: 'secretstores.azure.keyvault'
    version: 'v1'
    metadata: [
      {
        name: 'vaultName'
        value: KEYVAULT_NAME
      }
    ]
    scopes: [
      backendApiServiceName
      backendProcessorServiceName
    ]
  }
}

//Cosmos DB State Store Component
resource statestoreComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'statestore'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'state.azure.cosmosdb'
    version: 'v1'
    secrets: [
    ]
    metadata: [
      {
        name: 'url'
        value: cosmosDbAccount.properties.documentEndpoint
      }
      {
        name: 'database'
        value: cosmosDbDatabaseName
      }
      {
        name: 'collection'
        value: cosmosDbCollectionName
      }
    ]
    scopes: [
      backendApiServiceName
    ]
  }
}

//PubSub service bus Component
resource pubsubComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'dapr-pubsub-servicebus'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'pubsub.azure.servicebus'
    version: 'v1'
    secrets: [
    ]
    metadata: [
      {
        name: 'namespaceName'
        value: '${serviceBusName}.servicebus.windows.net'
      }
      {
        name: 'consumerID'
        value: backendProcessorServiceName
      }
    ]
    scopes: [
      backendApiServiceName
      backendProcessorServiceName
    ]
  }
}

//Scheduled Tasks Manager Component
resource scheduledtasksmanagerDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'scheduledtasksmanager'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'bindings.cron'
    version: 'v1'
    metadata: [
      {
        name: 'schedule'
        value: scheduledJobCron
      }
    ]
    scopes: [
      backendProcessorServiceName
    ]
  }
}

//External tasks manager Component (Storage Queue)
resource externaltasksmanagerDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'externaltasksmanager'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'bindings.azure.storagequeues'
    version: 'v1'
    secretStoreComponent: secretStoreComponentName
    metadata: [
      {
        name: 'storageAccount'
        value: storageAccountName
      }
      {
        name: 'queue'
        value: externalTasksQueueName
      }
      {
        name: 'decodeBase64'
        value: 'true'
      }
      {
        name: 'route'
        value: '/externaltasksprocessor/process'
      }
      {
        name: 'storageAccessKey'
        secretRef: externalStorageKeySecretName
      }
    ]
    scopes: [
      backendProcessorServiceName
    ]
  }
  dependsOn: [
    secretstoreComponent
  ]
}

//External tasks blob store Component (Blob Store)
resource externaltasksblobstoreDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'externaltasksblobstore'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'bindings.azure.blobstorage'
    version: 'v1'
    secretStoreComponent: secretStoreComponentName
    metadata: [
      {
        name: 'storageAccount'
        value: storageAccountName
      }
      {
        name: 'container'
        value: externalTasksContainerBlobName
      }
      {
        name: 'decodeBase64'
        value: 'false'
      }
      {
        name: 'publicAccessLevel'
        value: 'none'
      }
      {
        name: 'storageAccessKey'
        secretRef: externalStorageKeySecretName
      }
    ]
    scopes: [
      backendProcessorServiceName
    ]
  }
  dependsOn: [
    secretstoreComponent
  ]
}

//SendGrid outbound Component
resource sendgridDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-06-01-preview' = {
  name: 'sendgrid'
  parent: containerAppsEnvironment
  properties: {
    componentType: 'bindings.twilio.sendgrid'
    version: 'v1'
    secretStoreComponent: secretStoreComponentName
    metadata: [
      {
        name: 'emailFrom'
        value: sendGridEmailFrom
      }
      {
        name: 'emailFromName'
        value: sendGridEmailFromName
      }
      {
        name: 'apiKey'
        secretRef: sendGridKeySecretName
      }
    ]
    scopes: [
      backendProcessorServiceName
    ]
  }
  dependsOn:[
    secretstoreComponent
  ]
}
What we've added in the Bicep file above
  • This module will be responsible for creating all dapr components used in the solution. It accepts various input parameters needed by the dapr components.
  • Notice how we are using the keyword existing to obtain a strongly typed reference to the pre-created resource

    1
    2
    3
    4
    5
    6
    7
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
    name: containerAppsEnvironmentName
    }
    
    resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
    name: cosmosDbName
    }
    

8. Create Secrets Into Azure Key Vault

This module will have the responsibility of generating the secrets and saving them in Azure Key Vault. Additionally, it will establish a role assignment for the backend processor service, specifically of type Azure Role Key Vault Secrets User, which will allow the service to access the Key Vault and retrieve the secrets.

To achieve this, create a new directory called container-apps\secrets within the modules folder. Add new file as shown below under the folder bicep\modules\container-apps\secrets:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of the Key Vault.')
param KEYVAULT_NAME string

@description('The name of the secret containing the SendGrid API key value for the Backend Background Processor Service.')
param sendGridKeySecretName string

@secure()
@description('The SendGrid API key for for Backend Background Processor Service.')
param sendGridKeySecretValue string

@description('The name of the secret containing the External Azure Sorage Access key for the Backend Background Processor Service.')
param externalAzureStorageKeySecretName string

@secure()
@description('The External Azure Stroage Access key for the Backend Background Processor Service.')
param externalAzureStorageKeySecretValue string

@description('The principal ID of the Backend Processor Service.')
param backendProcessorServicePrincipalId string

// ------------------
// VARIABLES
// ------------------

var keyVaultSecretUserRoleGuid = '4633458b-17de-408a-b874-0445c86b69e6'

var sendGridKey = empty(sendGridKeySecretValue) ? 'dummy' : sendGridKeySecretValue

// ------------------
// RESOURCES
// ------------------

resource keyVault 'Microsoft.KeyVault/vaults@2021-04-01-preview' existing = {
  name: KEYVAULT_NAME
}

// Send Grid API key secret used by Backend Background Processor Service.
resource sendGridKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
  parent: keyVault
  tags: tags
  name: sendGridKeySecretName
  properties: {
    value: sendGridKey
  }
}

// External Azure storage key secret used by Backend Background Processor Service.
resource externalAzureStorageKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
  parent: keyVault
  tags: tags
  name: externalAzureStorageKeySecretName
  properties: {
    value: externalAzureStorageKeySecretValue
  }
}

resource keyVaultSecretUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().id, keyVault.id, backendProcessorServicePrincipalId, keyVaultSecretUserRoleGuid) 
  scope: keyVault
  properties: {
    principalId: backendProcessorServicePrincipalId
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretUserRoleGuid)
    principalType: 'ServicePrincipal'
  }
}

9. Define the Frontend Service Azure Container App

We will now begin defining the modules that are necessary for producing the container apps, starting with the Frontend App. To initiate this process, add a new file under the folder bicep\modules\container-apps as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The resource Id of the container apps environment.')
param containerAppsEnvironmentId string

@description('The name of the service for the frontend web app service. The name is use as Dapr App ID.')
param frontendWebAppServiceName string

// Container Registry & Image
@description('The name of the container registry.')
param containerRegistryName string

@description('The resource ID of the user assigned managed identity for the container registry to be able to pull images from it.')
param containerRegistryUserAssignedIdentityId string

@description('The image for the frontend web app service.')
param frontendWebAppServiceImage string

@secure()
@description('The Application Insights Instrumentation.')
param appInsightsInstrumentationKey string

@description('The target and dapr port for the frontend web app service.')
param frontendWebAppPortNumber int

// ------------------
// RESOURCES
// ------------------

resource frontendWebAppService 'Microsoft.App/containerApps@2022-06-01-preview' = {
  name: frontendWebAppServiceName
  location: location
  tags: tags
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
        '${containerRegistryUserAssignedIdentityId}': {}
    }
  }
  properties: {
    managedEnvironmentId: containerAppsEnvironmentId
    configuration: {
      activeRevisionsMode: 'single'
      ingress: {
        external: true
        targetPort: frontendWebAppPortNumber
      }
      dapr: {
        enabled: true
        appId: frontendWebAppServiceName
        appProtocol: 'http'
        appPort: frontendWebAppPortNumber
        logLevel: 'info'
        enableApiLogging: true
      }
      secrets: [
        {
          name: 'appinsights-key'
          value: appInsightsInstrumentationKey
        }
      ]
      registries: !empty(containerRegistryName) ? [
        {
          server: '${containerRegistryName}.azurecr.io'
          identity: containerRegistryUserAssignedIdentityId
        }
      ] : []
    }
    template: {
      containers: [
        {
          name: frontendWebAppServiceName
          image: frontendWebAppServiceImage
          resources: {
            cpu: json('0.25')
            memory: '0.5Gi'
          }
          env: [
            {
              name: 'ApplicationInsights__InstrumentationKey'
              secretRef: 'appinsights-key'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 1
      }
    }
  }
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the container app for the frontend web app service.')
output frontendWebAppServiceContainerAppName string = frontendWebAppService.name

@description('The FQDN of the frontend web app service.')
output frontendWebAppServiceFQDN string = frontendWebAppService.properties.configuration.ingress.fqdn
What we've added in the Bicep file above
  • Observe the usage of the @secure attribute on input parameters that contain confidential information or keys. This attribute may be applied to both string and object parameters that encompass secretive values. By implementing this attribute, Azure will abstain from presenting the parameter values within the deployment logs or on the terminal if you happen to be utilizing Azure CLI.
  • The output parameters of this module will provide the fully qualified domain name (FQDN) for the frontend container application.

10. Define the Backend Api Service Azure Container App

Add a new file under the folder bicep\modules\container-apps as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The resource Id of the container apps environment.')
param containerAppsEnvironmentId string

@description('The name of the service for the backend api service. The name is use as Dapr App ID.')
param backendApiServiceName string

// Service Bus
@description('The name of the service bus namespace.')
param serviceBusName string

@description('The name of the service bus topic.')
param serviceBusTopicName string

// Cosmos DB
@description('The name of the provisioned Cosmos DB resource.')
param cosmosDbName string 

@description('The name of the provisioned Cosmos DB\'s database.')
param cosmosDbDatabaseName string

@description('The name of Cosmos DB\'s collection.')
param cosmosDbCollectionName string

// Container Registry & Image
@description('The name of the container registry.')
param containerRegistryName string

@description('The resource ID of the user assigned managed identity for the container registry to be able to pull images from it.')
param containerRegistryUserAssignedIdentityId string

@description('The image for the backend api service.')
param backendApiServiceImage string

@secure()
@description('The Application Insights Instrumentation.')
param appInsightsInstrumentationKey string

@description('The target and dapr port for the backend api service.')
param backendApiPortNumber int

// ------------------
// RESOURCES
// ------------------

resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = {
  name: serviceBusName
}

resource serviceBusTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' existing = {
  name: serviceBusTopicName
  parent: serviceBusNamespace
}

resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
  name: cosmosDbName
}

resource cosmosDbDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-04-15' existing = {
  name: cosmosDbDatabaseName
  parent: cosmosDbAccount
}

resource cosmosDbDatabaseCollection 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-05-15' existing = {
  name: cosmosDbCollectionName
  parent: cosmosDbDatabase
}

resource backendApiService 'Microsoft.App/containerApps@2022-06-01-preview' = {
  name: backendApiServiceName
  location: location
  tags: tags
  identity: {
    type: 'SystemAssigned,UserAssigned'
    userAssignedIdentities: {
        '${containerRegistryUserAssignedIdentityId}': {}
    }
  }
  properties: {
    managedEnvironmentId: containerAppsEnvironmentId
    configuration: {
      activeRevisionsMode: 'single'
      ingress: {
        external: false
        targetPort: backendApiPortNumber
      }
      dapr: {
        enabled: true
        appId: backendApiServiceName
        appProtocol: 'http'
        appPort: backendApiPortNumber
        logLevel: 'info'
        enableApiLogging: true
      }
      registries: !empty(containerRegistryName) ? [
        {
          server: '${containerRegistryName}.azurecr.io'
          identity: containerRegistryUserAssignedIdentityId
        }
      ] : []
      secrets: [
        {
          name: 'appinsights-key'
          value: appInsightsInstrumentationKey
        }
      ]
    }
    template: {
      containers: [
        {
          name: backendApiServiceName
          image: backendApiServiceImage
          resources: {
            cpu: json('0.25')
            memory: '0.5Gi'
          }
          env: [
            {
              name: 'ApplicationInsights__InstrumentationKey'
              secretRef: 'appinsights-key'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 1
      }
    }
  }
}

// Assign cosmosdb account read/write access to aca system assigned identity
// To know more: https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac
resource backendApiService_cosmosdb_role_assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-08-15' = {
  name: guid(subscription().id, backendApiService.name, '00000000-0000-0000-0000-000000000002')
  parent: cosmosDbAccount
  properties: {
    principalId: backendApiService.identity.principalId
    roleDefinitionId:  resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', cosmosDbAccount.name, '00000000-0000-0000-0000-000000000002')//DocumentDB Data Contributor
    scope: '${cosmosDbAccount.id}/dbs/${cosmosDbDatabase.name}/colls/${cosmosDbDatabaseCollection.name}'
  }
}

// Enable publish message to Service Bus using app managed identity.
resource backendApiService_sb_role_assignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(resourceGroup().id, backendApiService.name, '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39')
  properties: {
    principalId: backendApiService.identity.principalId
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39')//Azure Service Bus Data Sender
    principalType: 'ServicePrincipal'
  }
  scope: serviceBusTopic
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the container app for the backend api service.')
output backendApiServiceContainerAppName string = backendApiService.name

@description('The FQDN of the backend api service.')
output backendApiServiceFQDN string = backendApiService.properties.configuration.ingress.fqdn
What we've added in the Bicep file above
  • Notice how we are assigning the Cosmosdb account a read/write access using the Cosmos DB Built-in Data Contributor role to the Backend API system assigned identity, by using the code below:

    1
    2
    3
    4
    5
    6
    7
    8
    resource backendApiService_cosmosdb_role_assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-08-15' = {
    name: guid(subscription().id, backendApiService.name, '00000000-0000-0000-0000-000000000002')
    parent: cosmosDbAccount
    properties: {
        principalId: backendApiService.identity.principalId
        roleDefinitionId:  resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', cosmosDbAccount.name, '00000000-0000-0000-0000-000000000002')//DocumentDB Data Contributor
        scope: '${cosmosDbAccount.id}/dbs/${cosmosDbDatabase.name}/colls/${cosmosDbDatabaseCollection.name}'
    }
    
  • A similar technique was applied when assigning the Azure Service Bus Data Sender role to the Backend API, enabling it to publish messages to Azure Service Bus utilizing the Backend API system-assigned identity. This was accomplished utilizing the following code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    resource backendApiService_sb_role_assignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
    name: guid(resourceGroup().id, backendApiService.name, '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39')
    properties: {
        principalId: backendApiService.identity.principalId
        roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39')//Azure Service Bus Data Sender
        principalType: 'ServicePrincipal'
    }
    scope: serviceBusTopic
    }
    

11. Define the Backend Processor Service Azure Container App

Add a new file under the folder bicep\modules\container-apps as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

@description('The resource Id of the container apps environment.')
param containerAppsEnvironmentId string

@description('The name of the service for the backend processor service. The name is use as Dapr App ID and as the name of service bus topic subscription.')
param backendProcessorServiceName string

// Key Vault
@description('The resource ID of the key vault to store the license key for the backend processor service.')
param keyVaultId string

@description('The name of the secret containing the SendGrid API key value for the Backend Background Processor Service.')
param sendGridKeySecretName string

@secure()
@description('The SendGrid API key for for Backend Background Processor Service.')
param sendGridKeySecretValue string

@description('The name of the secret containing the External Azure Storage Access key for the Backend Background Processor Service.')
param externalStorageKeySecretName string

@secure()
@description('The Application Insights Instrumentation.')
param appInsightsInstrumentationKey string

// Service Bus
@description('The name of the service bus namespace.')
param serviceBusName string

@description('The name of the service bus topic.')
param serviceBusTopicName string

@description('The name of the service bus topic\'s authorization rule.')
param serviceBusTopicAuthorizationRuleName string

// External Storage
@description('The name of the external Azure Storage Account.')
param externalStorageAccountName string

// Container Registry & Image
@description('The name of the container registry.')
param containerRegistryName string

@description('The resource ID of the user assigned managed identity for the container registry to be able to pull images from it.')
param containerRegistryUserAssignedIdentityId string

@description('The image for the backend processor service.')
param backendProcessorServiceImage string

@description('The dapr port for the backend processor service.')
param backendProcessorPortNumber int


// ------------------
// VARIABLES
// ------------------

var keyVaultIdTokens = split(keyVaultId, '/')
var keyVaultSubscriptionId = keyVaultIdTokens[2]
var keyVaultResourceGroupName = keyVaultIdTokens[4]
var KEYVAULT_NAME = keyVaultIdTokens[8]

// ------------------
// RESOURCES
// ------------------

resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = {
  name: serviceBusName
}

resource serviceBusTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' existing = {
  name: serviceBusTopicName
  parent: serviceBusNamespace
}

resource serviceBusTopicAuthorizationRule 'Microsoft.ServiceBus/namespaces/topics/authorizationRules@2021-11-01' existing = {
  name: serviceBusTopicAuthorizationRuleName
  parent: serviceBusTopic
}


resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = {
  name: externalStorageAccountName
}

resource backendProcessorService 'Microsoft.App/containerApps@2022-06-01-preview' = {
  name: backendProcessorServiceName
  location: location
  tags: tags
  identity: {
    type: 'SystemAssigned,UserAssigned'
    userAssignedIdentities: {
        '${containerRegistryUserAssignedIdentityId}': {}
    }
  }
  properties: {
    managedEnvironmentId: containerAppsEnvironmentId
    configuration: {
      activeRevisionsMode: 'single'
      dapr: {
        enabled: true
        appId: backendProcessorServiceName
        appProtocol: 'http'
        appPort: backendProcessorPortNumber
        logLevel: 'info'
        enableApiLogging: true
      }
      secrets: [
        {
          name: 'svcbus-connstring'
          value: serviceBusTopicAuthorizationRule.listKeys().primaryConnectionString
        }
        {
          name: 'appinsights-key'
          value: appInsightsInstrumentationKey
        }
      ]
      registries: !empty(containerRegistryName) ? [
        {
          server: '${containerRegistryName}.azurecr.io'
          identity: containerRegistryUserAssignedIdentityId
        }
      ] : []
    }
    template: {
      containers: [
        {
          name: backendProcessorServiceName
          image: backendProcessorServiceImage
          resources: {
            cpu: json('0.25')
            memory: '0.5Gi'
          }
          env: [
            {
              name: 'SendGrid__IntegrationEnabled'
              value: empty(sendGridKeySecretValue) ? 'false' : 'true'
            }
            {
              name: 'ApplicationInsights__InstrumentationKey'
              secretRef: 'appinsights-key'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 5
        rules: [
          {
            name: 'topic-msgs-length'
            custom: {
              type: 'azure-servicebus'
              auth: [
                {
                  secretRef: 'svcbus-connstring'
                  triggerParameter: 'connection'
                }
              ]
              metadata: {
                namespace: serviceBusName
                subscriptionName: backendProcessorServiceName
                topicName: serviceBusTopicName
                messageCount: '10'
                connectionFromEnv: 'svcbus-connstring'
              }
            }
          }
        ]
      }
    }
  }
}


// Enable consume from servicebus using system managed identity.
resource backendProcessorService_sb_role_assignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(resourceGroup().id, backendProcessorServiceName, '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0')
  properties: {
    principalId: backendProcessorService.identity.principalId
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0') // Azure Service Bus Data Receiver.
    principalType: 'ServicePrincipal'
  } 
  scope: serviceBusNamespace
}

// Invoke create secrets and assign role 'Azure Role Key Vault Secrets User' to the backend processor service
module backendProcessorKeySecret 'secrets/processor-backend-service-secrets.bicep' = {
  name: 'backendProcessorKeySecret-${uniqueString(resourceGroup().id)}'
  params: {
    KEYVAULT_NAME: KEYVAULT_NAME
    sendGridKeySecretName: sendGridKeySecretName
    sendGridKeySecretValue: sendGridKeySecretValue
    externalAzureStorageKeySecretName: externalStorageKeySecretName
    externalAzureStorageKeySecretValue: storageAccount.listKeys().keys[0].value
    backendProcessorServicePrincipalId: backendProcessorService.identity.principalId
  }
  scope: resourceGroup(keyVaultSubscriptionId, keyVaultResourceGroupName)
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the container app for the backend processor service.')
output backendProcessorServiceContainerAppName string = backendProcessorService.name
What we've added in the Bicep file above
  • Notice how we are assigning the role Azure Service Bus Data Receiver to the Backend Processor to be able to consume/read messages from Azure Service Bus Topic using Backend Processor system assigned identity, by using the code below:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    resource backendProcessorService_sb_role_assignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
    name: guid(resourceGroup().id, backendProcessorServiceName, '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0')
    properties: {
        principalId: backendProcessorService.identity.principalId
        roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0') // Azure Service Bus Data Receiver.
        principalType: 'ServicePrincipal'
    } 
    scope: serviceBusNamespace
    }
    
  • Within this module, we've invoked the module defined in step 8 which is responsible to create the secrets in Azure Key Vault and assign the role Azure Role Key Vault Secrets User to the Backend Processor Service, by using the code below:
    module backendProcessorKeySecret 'secrets/processor-backend-service-secrets.bicep' = {
    name: 'backendProcessorKeySecret-${uniqueString(resourceGroup().id)}'
    params: {
        KEYVAULT_NAME: KEYVAULT_NAME
        sendGridKeySecretName: sendGridKeySecretName
        sendGridKeySecretValue: sendGridKeySecretValue
        externalAzureStorageKeySecretName: externalStorageKeySecretName
        externalAzureStorageKeySecretValue: storageAccount.listKeys().keys[0].value
        backendProcessorServicePrincipalId: backendProcessorService.identity.principalId
    }
    scope: resourceGroup(keyVaultSubscriptionId, keyVaultResourceGroupName)
    }
    

12. Define a Container Module For the Three Container Apps

This module will act as a container for the three Container Apps modules defined in the previous three steps. It is optional to create it, but it makes it easier when we invoke all the created modules as you will see in the next step.

Add a new file under the folder bicep\modules as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('The tags to be assigned to the created resources.')
param tags object = {}

@description('The name of the container apps environment.')
param containerAppsEnvironmentName string

// Services
@description('The name of the service for the backend api service. The name is use as Dapr App ID.')
param backendApiServiceName string

@description('The name of the service for the backend processor service. The name is use as Dapr App ID and as the name of service bus topic subscription.')
param backendProcessorServiceName string

@description('The name of the service for the frontend web app service. The name is use as Dapr App ID.')
param frontendWebAppServiceName string

// Service Bus
@description('The name of the service bus namespace.')
param serviceBusName string

@description('The name of the service bus topic.')
param serviceBusTopicName string

@description('The name of the service bus topic\'s authorization rule.')
param serviceBusTopicAuthorizationRuleName string

// Cosmos DB
@description('The name of the provisioned Cosmos DB resource.')
param cosmosDbName string 

@description('The name of the provisioned Cosmos DB\'s database.')
param cosmosDbDatabaseName string

@description('The name of Cosmos DB\'s collection.')
param cosmosDbCollectionName string

// Key Vault
@description('The resource ID of the key vault.')
param keyVaultId string

@description('The name of the secret containing the SendGrid API key value for the Backend Background Processor Service.')
param sendGridKeySecretName string

@secure()
@description('The SendGrid API key for for Backend Background Processor Service.')
param sendGridKeySecretValue string

@description('The name of the secret containing the External Azure Storage Access key for the Backend Background Processor Service.')
param externalStorageKeySecretName string

// External Storage
@description('The name of the external Azure Storage Account.')
param externalStorageAccountName string

// Container Registry & Images
@description('The name of the container registry.')
param containerRegistryName string

@description('The image for the backend api service.')
param backendApiServiceImage string

@description('The image for the backend processor service.')
param backendProcessorServiceImage string

@description('The image for the frontend web app service.')
param frontendWebAppServiceImage string

@description('The name of the application insights.')
param applicationInsightsName string

// App Ports
@description('The target and dapr port for the frontend web app service.')
param frontendWebAppPortNumber int

@description('The target and dapr port for the backend api service.')
param backendApiPortNumber int

@description('The dapr port for the backend processor service.')
param backendProcessorPortNumber int

// ------------------
// VARIABLES
// ------------------

var containerRegistryPullRoleGuid='7f951dda-4ed3-4680-a7ca-43fe172d538d'

// ------------------
// RESOURCES
// ------------------

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
  name: containerAppsEnvironmentName
}

//Reference to AppInsights resource
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
  name: applicationInsightsName
}

resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = {
  name: containerRegistryName
}

resource containerRegistryUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'aca-user-identity-${uniqueString(resourceGroup().id)}'
  location: location
  tags: tags
}

resource containerRegistryPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if(!empty(containerRegistryName)) {
  name: guid(subscription().id, containerRegistry.id, containerRegistryUserAssignedIdentity.id) 
  scope: containerRegistry
  properties: {
    principalId: containerRegistryUserAssignedIdentity.properties.principalId
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', containerRegistryPullRoleGuid)
    principalType: 'ServicePrincipal'
  }
}

module frontendWebAppService 'container-apps/webapp-frontend-service.bicep' = {
  name: 'frontendWebAppService-${uniqueString(resourceGroup().id)}'
  params: {
    frontendWebAppServiceName: frontendWebAppServiceName
    location: location
    tags: tags
    containerAppsEnvironmentId: containerAppsEnvironment.id
    containerRegistryName: containerRegistryName
    containerRegistryUserAssignedIdentityId: containerRegistryUserAssignedIdentity.id
    frontendWebAppServiceImage: frontendWebAppServiceImage
    appInsightsInstrumentationKey: applicationInsights.properties.InstrumentationKey
    frontendWebAppPortNumber: frontendWebAppPortNumber

  }
}

module backendApiService 'container-apps/webapi-backend-service.bicep' = {
  name: 'backendApiService-${uniqueString(resourceGroup().id)}'
  params: {
    backendApiServiceName: backendApiServiceName
    location: location
    tags: tags
    containerAppsEnvironmentId: containerAppsEnvironment.id
    serviceBusName: serviceBusName
    serviceBusTopicName: serviceBusTopicName
    containerRegistryName: containerRegistryName
    containerRegistryUserAssignedIdentityId: containerRegistryUserAssignedIdentity.id
    backendApiServiceImage: backendApiServiceImage
    cosmosDbName: cosmosDbName
    cosmosDbDatabaseName: cosmosDbDatabaseName
    cosmosDbCollectionName: cosmosDbCollectionName
    appInsightsInstrumentationKey: applicationInsights.properties.InstrumentationKey
    backendApiPortNumber: backendApiPortNumber
  }
}

module backendProcessorService 'container-apps/processor-backend-service.bicep' = {
  name: 'backendProcessorService-${uniqueString(resourceGroup().id)}'
  params: {
    backendProcessorServiceName: backendProcessorServiceName
    location: location
    tags: tags
    containerAppsEnvironmentId: containerAppsEnvironment.id
    keyVaultId: keyVaultId
    serviceBusName: serviceBusName
    serviceBusTopicName: serviceBusTopicName
    serviceBusTopicAuthorizationRuleName: serviceBusTopicAuthorizationRuleName
    containerRegistryName: containerRegistryName
    containerRegistryUserAssignedIdentityId: containerRegistryUserAssignedIdentity.id
    sendGridKeySecretName: sendGridKeySecretName
    sendGridKeySecretValue: sendGridKeySecretValue
    externalStorageAccountName: externalStorageAccountName
    externalStorageKeySecretName:externalStorageKeySecretName
    backendProcessorServiceImage: backendProcessorServiceImage
    appInsightsInstrumentationKey: applicationInsights.properties.InstrumentationKey
    backendProcessorPortNumber: backendProcessorPortNumber
  }
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the container app for the backend processor service.')
output backendProcessorServiceContainerAppName string = backendProcessorService.outputs.backendProcessorServiceContainerAppName

@description('The name of the container app for the backend api service.')
output backendApiServiceContainerAppName string = backendApiService.outputs.backendApiServiceContainerAppName

@description('The name of the container app for the front end web app service.')
output frontendWebAppServiceContainerAppName string = frontendWebAppService.outputs.frontendWebAppServiceContainerAppName

@description('The FQDN of the front end web app.')
output frontendWebAppServiceFQDN string = frontendWebAppService.outputs.frontendWebAppServiceFQDN

@description('The FQDN of the backend web app')
output backendApiServiceFQDN string  = backendApiService.outputs.backendApiServiceFQDN

13. Define the Main Module For the Solution

Finally, we must specify the Main Bicep module that will connect all other modules together. This file will be referenced by the AZ CLI command when producing all resources.

To achieve this, add a new file under the bicep directory as shown below:

targetScope = 'resourceGroup'

// ------------------
//    PARAMETERS
// ------------------

@description('The location where the resources will be created.')
param location string = resourceGroup().location

@description('Optional. The prefix to be used for all resources created by this template.')
param prefix string = ''

@description('Optional. The suffix to be used for all resources created by this template.')
param suffix string = ''

@description('Optional. The tags to be assigned to the created resources.')
param tags object = {}

// Container Apps Env / Log Analytics Workspace / Application Insights
@description('Optional. The name of the container apps environment. If set, it overrides the name generated by the template.')
param containerAppsEnvironmentName string = '${prefix}cae-${uniqueString(resourceGroup().id)}${suffix}'

@description('Optional. The name of the log analytics workspace. If set, it overrides the name generated by the template.')
param logAnalyticsWorkspaceName string = '${prefix}log-${uniqueString(resourceGroup().id)}${suffix}'

@description('Optional. The name of the application insights. If set, it overrides the name generated by the template.')
param applicationInsightName string = '${prefix}appi-${uniqueString(resourceGroup().id)}${suffix}'

// Servivces
@description('The name of the service for the backend processor service. The name is use as Dapr App ID and as the name of service bus topic subscription.')
param backendProcessorServiceName string

@description('The name of the service for the backend api service. The name is use as Dapr App ID.')
param backendApiServiceName string

@description('The name of the service for the frontend web app service. The name is use as Dapr App ID.')
param frontendWebAppServiceName string

// Service Bus
@description('Optional. The name of the service bus namespace. If set, it overrides the name generated by the template.')
param serviceBusName string = '${prefix}sb-${uniqueString(resourceGroup().id)}${suffix}'

@description('The name of the service bus topic.')
param serviceBusTopicName string

@description('The name of the service bus topic\'s authorization rule.')
param serviceBusTopicAuthorizationRuleName string

// Cosmos DB
@description('Optional. The name of Cosmos DB resource. If set, it overrides the name generated by the template.')
param cosmosDbName string ='${prefix}cosno-${uniqueString(resourceGroup().id)}${suffix}'

@description('The name of Cosmos DB\'s database.')
param cosmosDbDatabaseName string

@description('The name of Cosmos DB\'s collection.')
param cosmosDbCollectionName string

// Azure Stroage
@description('The name of the external Azure Storage Account.')
param storageAccountName string = '${prefix}storage${uniqueString(resourceGroup().id)}${suffix}'

@description('The name of the external Queue in Azure Storage.')
param externalTasksQueueName string

@description('The name of the external blob container in Azure Storage.')
param externalTasksContainerBlobName string

@description('The name of the secret containing the External Azure Storage Access key for the backend processor service.')
param externalStorageKeySecretName string 

//SendGrid
@description('The name of the secret containing the SendGrid API key value for the backend processor service.')
param sendGridKeySecretName string = 'sendgrid-api-key'

@description('The name of the SendGrid Email From.')
param sendGridEmailFrom string

@description('The name of the SendGrid Email From Name.')
param sendGridEmailFromName string

@secure()
@description('The SendGrid API key for the backend processor service. If not provided, SendGrid integration will be disabled.')
param sendGridKeySecretValue string

//Cron Shedule Jon
@description('The cron settings for scheduled job.')
param scheduledJobCron string

// Dapr components
@description('The name of Dapr component for the secret store building block.')
// We disable lint of this line as it is not a secret but the name of the Dapr component
#disable-next-line secure-secrets-in-params
param secretStoreComponentName string

@description('The key vault name store secrets')
param KEYVAULT_NAME string = '${prefix}kv-${uniqueString(resourceGroup().id)}${suffix}'

// Container Registry & Images
@description('The name of the container registry.')
param containerRegistryName string

@description('The image for the backend processor service.')
param backendProcessorServiceImage string

@description('The image for the backend api service.')
param backendApiServiceImage string

@description('The image for the frontend web app service.')
param frontendWebAppServiceImage string

// App Ports
@description('The target and dapr port for the frontend web app service.')
param frontendWebAppPortNumber int = 80

@description('The target and dapr port for the backend api service.')
param backendApiPortNumber int = 80

@description('The dapr port for the backend processor service.')
param backendProcessorPortNumber int = 80

// ------------------
// RESOURCES
// ------------------

module containerAppsEnvironment 'modules/container-apps-environment.bicep' ={
  name: 'containerAppsEnv-${uniqueString(resourceGroup().id)}'
  params: {
   containerAppsEnvironmentName: containerAppsEnvironmentName
   logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
   applicationInsightName: applicationInsightName
    location: location
    tags: tags
  }
}

module keyVault 'modules/key-vault.bicep' = {
  name: 'keyVault-${uniqueString(resourceGroup().id)}'
  params: {
    KEYVAULT_NAME: KEYVAULT_NAME
    location: location
    tags: tags
  }
}

module serviceBus 'modules/service-bus.bicep' = {
  name: 'serviceBus-${uniqueString(resourceGroup().id)}'
  params: {
    serviceBusName: serviceBusName
    location: location
    tags: tags
    serviceBusTopicName: serviceBusTopicName
    serviceBusTopicAuthorizationRuleName: serviceBusTopicAuthorizationRuleName
    backendProcessorServiceName: backendProcessorServiceName
  }
}

module cosmosDb 'modules/cosmos-db.bicep' = {
  name: 'cosmosDb-${uniqueString(resourceGroup().id)}'
  params: {
    cosmosDbName: cosmosDbName
    location: location
    tags: tags
    cosmosDbDatabaseName: cosmosDbDatabaseName
    cosmosDbCollectionName: cosmosDbCollectionName 
  }
}

module externalStorageAccount 'modules/storage-account.bicep' = {
  name: 'storageAccount-${uniqueString(resourceGroup().id)}'
  params: {
    storageAccountName: storageAccountName
    externalTasksQueueName: externalTasksQueueName
    location: location
    tags: tags
  }
}

module daprComponents 'modules/dapr-components.bicep' = {
  name: 'daprComponents-${uniqueString(resourceGroup().id)}'
  params: {
    secretStoreComponentName: secretStoreComponentName 
    containerAppsEnvironmentName: containerAppsEnvironmentName    
    KEYVAULT_NAME: KEYVAULT_NAME    
    serviceBusName: serviceBus.outputs.serviceBusName
    cosmosDbName: cosmosDb.outputs.cosmosDbName
    cosmosDbDatabaseName: cosmosDb.outputs.cosmosDbDatabaseName
    cosmosDbCollectionName: cosmosDb.outputs.cosmosDbCollectionName    
    backendApiServiceName: backendApiServiceName
    backendProcessorServiceName: backendProcessorServiceName
    storageAccountName: storageAccountName
    sendGridKeySecretName: sendGridKeySecretName
    sendGridEmailFrom: sendGridEmailFrom
    sendGridEmailFromName: sendGridEmailFromName
    scheduledJobCron: scheduledJobCron
    externalTasksQueueName: externalTasksQueueName
    externalTasksContainerBlobName: externalTasksContainerBlobName
    externalStorageKeySecretName: externalStorageKeySecretName
  }
  dependsOn: [
    containerAppsEnvironment
  ]
}

module containerApps 'modules/container-apps.bicep' = {
  name: 'containerApps-${uniqueString(resourceGroup().id)}'
  params: {
    location: location
    tags: tags
    backendProcessorServiceName: backendProcessorServiceName
    backendApiServiceName: backendApiServiceName
    frontendWebAppServiceName: frontendWebAppServiceName    
    containerAppsEnvironmentName: containerAppsEnvironmentName
    keyVaultId: keyVault.outputs.keyVaultId
    serviceBusName: serviceBus.outputs.serviceBusName
    serviceBusTopicName: serviceBus.outputs.serviceBusTopicName
    serviceBusTopicAuthorizationRuleName: serviceBus.outputs.serviceBusTopicAuthorizationRuleName    
    cosmosDbName: cosmosDb.outputs.cosmosDbName
    cosmosDbDatabaseName: cosmosDb.outputs.cosmosDbDatabaseName
    cosmosDbCollectionName: cosmosDb.outputs.cosmosDbCollectionName    
    containerRegistryName: containerRegistryName
    backendProcessorServiceImage: backendProcessorServiceImage
    backendApiServiceImage: backendApiServiceImage
    frontendWebAppServiceImage: frontendWebAppServiceImage
    sendGridKeySecretName: sendGridKeySecretName
    sendGridKeySecretValue: sendGridKeySecretValue
    applicationInsightsName: containerAppsEnvironment.outputs.applicationInsightsName
    externalStorageAccountName: externalStorageAccount.outputs.storageAccountName
    externalStorageKeySecretName: externalStorageKeySecretName
    frontendWebAppPortNumber: frontendWebAppPortNumber
    backendApiPortNumber: backendApiPortNumber
    backendProcessorPortNumber: backendProcessorPortNumber
  }
  dependsOn: [
    daprComponents
  ]
}

// ------------------
// OUTPUTS
// ------------------

@description('The name of the container app for the backend processor service.')
output backendProcessorServiceContainerAppName string = containerApps.outputs.backendProcessorServiceContainerAppName

@description('The name of the container app for the backend api service.')
output backendApiServiceContainerAppName string = containerApps.outputs.backendApiServiceContainerAppName

@description('The name of the container app for the front end web app service.')
output frontendWebAppServiceContainerAppName string = containerApps.outputs.frontendWebAppServiceContainerAppName

@description('The FQDN of the front end web app.')
output frontendWebAppServiceFQDN string = containerApps.outputs.frontendWebAppServiceFQDN

@description('The FQDN of the backend web app')
output backendApiServiceFQDN string  = containerApps.outputs.backendApiServiceFQDN
What we've added in the Bicep file above
  • When calling the module dapr-components.bicep we are setting the value of the array dependsOn to the Container Apps Environment. This is called explicit dependency which aids the Bicep interpreter in comprehending the relationships between components. In this instance, the Container Apps Environment must be provisioned before the Dapr Components to guarantee a successful deployment.

  • When calling the module container-apps.bicep, some of the input params are expecting are referencing another resource, for example consider the input param named cosmosDbName and the value used is cosmosDb.outputs.cosmosDbName. This means that the module cosmos-db.bicep should be created successfully before creating the container apps module, this called Implicit dependency.

Deploy the Infrastructure and Create the Components

Start by creating a new resource group which will contain all the resources to be created by the Bicep scripts.

1
2
3
4
5
6
$RESOURCE_GROUP="<your RG name>"
$LOCATION="<your location>"

az group create `
--name $RESOURCE_GROUP `
--location $LOCATION

Create a parameters file which will simplify the invocation of the main bicep file. To achieve this, right click on file main.bicep and select Generate Parameter File. This will result in creating a file named main.parameters.json similar to the file below:

Example
{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "value": ""
    },
    "suffix": {
        "value": ""
    },
    "tags": {
        "value": {}
    },
    "backendProcessorServiceName": {
      "value": "tasksmanager-backend-processor"
    },
    "backendApiServiceName": {
      "value": "tasksmanager-backend-api"
    },
    "frontendWebAppServiceName": {
      "value": "tasksmanager-frontend-webapp"
    },
    "serviceBusTopicName": {
      "value": "tasksavedtopic"
    },
    "serviceBusTopicAuthorizationRuleName": {
      "value": "tasksavedtopic-manage-policy"
    },
    "cosmosDbDatabaseName": {
      "value": "tasksmanagerdb"
    },
    "cosmosDbCollectionName": {
      "value": "taskscollection"
    },
    "sendGridKeySecretValue": {
      "value": ""
    },
    "sendGridEmailFrom": {
      "value": "<SEND_GRID_FROM_EMAIL>"
    },
    "sendGridEmailFromName": {
      "value": "Tasks Tracker Notification"
    },
    "externalTasksQueueName": {
      "value": "external-tasks-queue"
    },
    "externalTasksContainerBlobName": {
      "value": "externaltasksblob"
    },
    "externalStorageKeySecretName": {
      "value": "external-azure-storage-key"
    },
    "scheduledJobCron": {
      "value": "5 0 * * *"
    },
    "secretStoreComponentName": {
      "value": "secretstoreakv"
    },
    "containerRegistryName": {
      "value": "<CONTAINER_REGISTRY_NAME>"
    },
    "backendProcessorServiceImage": {
      "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-processor:latest"
    },
    "backendApiServiceImage": {
      "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-api:latest"
    },
    "frontendWebAppServiceImage": {
      "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-frontend-webapp:latest"
    },
    "frontendWebAppPortNumber": {
      "value": 80
    },
    "backendApiPortNumber": {
      "value": 80
    },
    "backendProcessorPortNumber": {
      "value": 80
    }
  }
}

Note

To use this file, you need to edit this generated file and provide values for the parameters. You can use the same values shown above in sample file.

You only need to replace parameter values between the angle brackets <> with values related to your container registry and SendGrid. Values for container registry and container images can be derived by following one of the three options in next step.

In case you followed along with the whole workshop and would like to use your own sourcecode, make sure to replace the port numbers (80) by the port numbers that were generated when you created your docker files in vs code for your three applications (e.g., 5225). Make sure that the port numbers match the numbers in the respective docker files. If port numbers aren't matching, your deployment will work without errors, but the portal will report issues with the apps and calling one of the apps will result in a session timeout.

Next, we will prepare container images for the three container apps and update the values in main.parameters.json file. You can do so by any of the three options below:

  1. Create an Azure Container Registry (ACR) inside the newly created Resource Group:

    1
    2
    3
    4
    5
    6
    $CONTAINER_REGISTRY_NAME="<your ACR name>"
    
    az acr create `
        --resource-group $RESOURCE_GROUP `
        --name $CONTAINER_REGISTRY_NAME `
        --sku Basic
    
  2. Build and push the images to ACR. Make sure you are at the root project directory when executing the following commands:

    ## Build Backend API on ACR and Push to ACR
    
    az acr build --registry $CONTAINER_REGISTRY_NAME `
        --image "tasksmanager/tasksmanager-backend-api" `
        --file 'TasksTracker.TasksManager.Backend.Api/Dockerfile' .
    
    ## Build Backend Service on ACR and Push to ACR
    
    az acr build --registry $CONTAINER_REGISTRY_NAME `
        --image "tasksmanager/tasksmanager-backend-processor" `
        --file 'TasksTracker.Processor.Backend.Svc/Dockerfile' .
    
    ## Build Frontend Web App on ACR and Push to ACR
    
    az acr build --registry $CONTAINER_REGISTRY_NAME `
        --image "tasksmanager/tasksmanager-frontend-webapp" `
        --file 'TasksTracker.WebPortal.Frontend.Ui/Dockerfile' .
    
  3. Update the main.parameters.json file with the container registry name and the container images names as shown below:

    {
        "containerRegistryName": {
            "value": "<CONTAINER_REGISTRY_NAME>"
        },
        "backendProcessorServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-processor:latest"
        },
        "backendApiServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-api:latest"
        },
        "frontendWebAppServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-frontend-webapp:latest"
        }
    }
    

All the container image are available in a public image repository. If you do not wish to build the container images from code directly, you can import it directly into your private container instance as shown below.

  1. Create an Azure Container Registry (ACR) inside the newly created Resource Group:

    1
    2
    3
    4
    5
    6
    $CONTAINER_REGISTRY_NAME="<your ACR name>"
    
    az acr create `
        --resource-group $RESOURCE_GROUP `
        --name $CONTAINER_REGISTRY_NAME `
        --sku Basic
    
  2. Import the images to your private ACR as shown below:

        az acr import `
        --name $CONTAINER_REGISTRY_NAME `
        --image tasksmanager/tasksmanager-backend-api `
        --source ghcr.io/azure/tasksmanager-backend-api:latest
    
        az acr import  `
        --name $CONTAINER_REGISTRY_NAME `
        --image tasksmanager/tasksmanager-frontend-webapp `
        --source ghcr.io/azure/tasksmanager-frontend-webapp:latest
    
        az acr import  `
        --name $CONTAINER_REGISTRY_NAME `
        --image tasksmanager/tasksmanager-backend-processor `
        --source ghcr.io/azure/tasksmanager-backend-processor:latest
    
  3. Update the main.parameters.json file with the container registry name and the container images names as shown below:

    {
        "containerRegistryName": {
            "value": "<CONTAINER_REGISTRY_NAME>"
        },
        "backendProcessorServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-processor:latest"
        },
        "backendApiServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-backend-api:latest"
        },
        "frontendWebAppServiceImage": {
            "value": "<CONTAINER_REGISTRY_NAME>.azurecr.io/tasksmanager/tasksmanager-frontend-webapp:latest"
        }
    }
    

All the container image are available in a public image repository. If you do not wish to build the container images from code directly, you can use the pre-built images from the public repository as shown below.

The public images can be set directly in the main.parameters.json file:

{
    "containerRegistryName": {
        "value": ""
    },
    "backendProcessorServiceImage": {
      "value": "ghcr.io/azure/tasksmanager-backend-processor:latest"
    },
    "backendApiServiceImage": {
      "value": "ghcr.io/azure/tasksmanager-backend-api:latest"
    },
    "frontendWebAppServiceImage": {
      "value": "ghcr.io/azure/tasksmanager-frontend-webapp:latest"
    },
}   

Start the deployment by calling az deployment group create. To accomplish this, open the PowerShell console and use the content below.

1
2
3
4
az deployment group create `
--resource-group $RESOURCE_GROUP `
--template-file "./bicep/main.bicep" `
--parameters "./bicep/main.parameters.json"

The Azure CLI will take the Bicep module and start creating the deployment in the resource group.

Verify the Final Results

Success

Upon successful deployment, you should observe all resources generated within the designated resource group. Additionally, you may navigate to the Deployments section to confirm that the ARM templates have been deployed, which should resemble the image provided below:

aca-deployment