Demystifying Azure Container Apps & Dapr – Part 2

Part 1 of this article is here.

Last time, I created an empty Azure Container Apps Environment using Bicep. I also created some required components like Key vaults, Log analytics workspace, Application Insights and Computer vision.

Application Architecture

Today I want to make progress on that. As I have briefly explained before, I want to have a frontend where users will upload photos and the frontend will store the image into the storage account as a blob. And here I want to draw my microservice boundary. I want another service to be triggered every time a new blob has been added to that storage account.  

This particular scenario is relatively common (I alone been into this kind of setup multiple time in past few years), where we want to get a shoulder tap when a blob has been uploaded (or modified). Many often deal such cases in Azure Functions (or Web Jobs) and go with the blob binding feature.

Few years before, I learned a lesson not to use blob triggers in any serious application, Azure Function’s blob trigger feature scans the container to track changes and if you have a large blob container these scanning quickly becomes expensive. Hence, my preferred method in such cases is to leverage Azure Event Grid Topic and redirect the events to either Azure Storage Queue or Azure service bus queue or topics. In this learning exercise, I will be using service bus topics. Then my backend microservice can consume that service bus topic for listening any blob changes in the blob container.

There are quite a few things we need then:

  • Storage Account
  • A blob container in that account
  • Event grid topic that listens to the storage account
  • Managed Identity will be used by the Event grid topic (so I won’t have to spread the storage account keys)
  • Service Bus Namespace and a topic
  • Configure Event grid topic to dispatch the messages to the service bus topic.
  • Assign roles to the Event grid managed identity so it can access both the storage account and the service bus.

Seems like I will be writing some Bicep again. Let’s do it.

Storage Account and Container

Here’re the interesting bits of the Bicep snippets, the entire source code can be found into the GitHub repository.

resource mainstorage 'Microsoft.Storage/storageAccounts@2021-02-01' = {
  name: accountName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
}

resource mainstoragecontainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' = {
  name: '${mainstorage.name}/default/${containerName}'
}

Event Grid Topic with Managed Identity

resource eventGridSystemTopic 'Microsoft.EventGrid/systemTopics@2022-06-15' = {
  name: eventGridSystemTopicName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    source: storageAccount.id
    topicType: 'Microsoft.Storage.StorageAccounts'
  }
}

Service Bus Namespace and Topic

Let’s create our service bus namespace and topic now.

resource sbNamespace 'Microsoft.ServiceBus/namespaces@2022-01-01-preview' = {
  name: serviceBusNamespace
  location: location
  sku: {
    name: 'Standard'
    tier: 'Standard'
  }
  properties: {
    publicNetworkAccess: 'Enabled'
    disableLocalAuth: false
    zoneRedundant: false
    minimumTlsVersion: '1.2'
  }
}

resource topic 'Microsoft.ServiceBus/namespaces/topics@2022-01-01-preview' = {
  name: '${serviceBusNamespace}/${serviceBusTopicName}'
  dependsOn: [
   sbNamespace 
  ]
  properties: {
    maxMessageSizeInKilobytes: 256
    defaultMessageTimeToLive: 'P14D'
    maxSizeInMegabytes: 1024
    requiresDuplicateDetection: false
    duplicateDetectionHistoryTimeWindow: 'PT10M'
    enableBatchedOperations: true
    status: 'Active'
    supportOrdering: true
    autoDeleteOnIdle: 'P10675199DT2H48M5.4775807S'
    enablePartitioning: false
    enableExpress: false 
  }
}

Dispatch Event grid messages to Service Bus topic

I was impressed that Azure is embracing the Cloud event schema for the event payloads. CloudEvents simplifies interoperability by providing a common event schema for publishing, and consuming cloud-based events. This schema allows for uniform tooling, standard ways of routing & handling events, and universal ways of deserializing the outer event schema. With a common schema, you can more easily integrate work across platforms.

resource eventgridTopicSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2022-06-15' = {
  name: '${eventGridSystemTopic.name}/xenielsubscription'
  properties: {
    eventDeliverySchema: 'CloudEventSchemaV1_0'
    destination: {
      endpointType: 'ServiceBusTopic'
      properties: {
        resourceId: topic.id
      }
    }
    filter: {
      includedEventTypes: [
        'Microsoft.Storage.BlobCreated'
        'Microsoft.Storage.BlobDeleted'
      ]
      enableAdvancedFilteringOnArrays: true      
    }
    retryPolicy: {
      maxDeliveryAttempts: 30
      eventTimeToLiveInMinutes: 1440
    }
  }
}

Role Assignments

We will be assigning appropriate roles to the Event Grid’s managed identity so that I can speak to storage account and service bus.

@description('This is the built-in Contributor role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles')
var roleDefinitionId  = 'b24988ac-6180-42a0-ab88-20f7382dd24c'

resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {  
  name: roleDefinitionId
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: storageAccount
  name: guid(eventGridSystemTopicName)
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: eventGridSystemTopic.identity.principalId    
  }
}

resource roleAssignmentForSystemIdentity 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: serviceBus
  name: guid('${eventGridSystemTopicName}-managedidentity')
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: eventGridSystemTopic.identity.principalId    
  }
}

At this point, I have the necessary infrastructure, where if I upload a blob into the container (from Azure portal) I will see a corresponding ‘event’ message into the service bus topic.

With that I can focus on my backend application that should listen for those service bus event messages. And here comes my first Dapr component.

Dapr Pub-sub

I will be using a PubSub component from Dapr that allows me to listen for messages in an Azure Service Bus topic. Generally, I used to create Dapr component as YAML syntax and they were just Kubernetes CRDs (Customer Resource Definition). However, in Azure Container Apps we need to put that in Bicep syntax, which is merely a trivial translation exercise. Here is what it looks like:

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

resource storageQueueBindingComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-03-01' = {
  name: componentName
  parent: environment
  properties: {
    componentType: 'pubsub.azure.servicebus'
    version: 'v1'
    ignoreErrors: false
    initTimeout: '5s'    
    metadata: [
      {
        name: 'namespaceName'
        // NOTE: Dapr expects just the domain name.
        value: replace(replace(serviceBusEndpoint, 'https://', ''), ':443/', '')
      }
      {
        name: 'azureClientId'
        value: managedIdentityClientId
      }
      {
        name: 'timeoutInSec'
        value: '60'
      }
    ]
    scopes: appScopes
  }
}

To be continued

That’s all for now. In the next part, I will be moving on configuring Dapr secret stores. The entire source code can be found in GitHub – if you want to take a look.

Thanks for reading.  

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s