.NET · Architecture · Azure Container Registry · AzureContainerApps · Bicep · Event Grid · managed-identity

Demystifying Azure Container Apps & Dapr – Part 3

Read Part 1 here.

Read Part 2 here.

Last time, I created an empty Azure Container Apps Environment, some supporting components like Key vaults, Log analytics workspace, Application Insights and Computer vision. I have also created an Event Grid topic that subscribed changes in Azure Storage container and dispatches change events to a Service Bus topic, all in Bicep and. Managed Identity was used, so we didn’t have any secrets to deal with in our code.

Dapr pubsub Component

Today I want to create a backend service (without ingress) that will process incoming messages into the Service Bus topic, but via Dapr pubsub component. So, our code will be ignorant about the existence of the Service Bus topic. I will be using .net for this (because I love it) but it could be any of your language of choice – as long as it can talk HTTP.

Our service needs to tell Dapr that it is interested in receiving messages to a certain topic of the service bus – that I configured last time. I will accomplish this via an API endpoint which Dapr will invoke during the container launch time (by the Dapr sidecar). Here’s how we can do that:

app.MapGet("/dapr/subscribe", async () => {
    await Task.CompletedTask;
    return Results.Json(new DaprSubscription[] { ConfigSettings.Subscription });
});

Now Dapr knows that, we are interested in receiving messages on that topic and we have also provided the endpoint where we expect the messages to dispatched. I find this design quite elegant, as my code doesn’t know anything about Service Bus, it’s a plain HTTP API and testing this is super easy.

app.MapPost(ConfigSettings.Route, async c => {
    Console.WriteLine("#### >>> We got called !!! ");
});

Once we receive messages, we will be reading the blob content and send it to the Computer Vision API to for image recognition. Therefore, our backend service needs to retrieve the Computer Vision secret from somewhere. I will be using Dapr secret store for that, specifically, Azure Key vault backed secret store. (Remember, we already put the Computer vision API key into the key vault in part 1 of this article?)

Key vault backed Dapr Secret store

We will have to define this secret store using Bicep. Here is how it looks like:

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

resource kvSecretStoreComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-03-01' = {
  name: componentName
  parent: environment
  properties: {
    componentType: 'secretstores.azure.keyvault'
    version: 'v1'
    ignoreErrors: false
    initTimeout: '5s'    
    metadata: [
      {
        name: 'vaultName'
        value: keyVaultName
      }
      {
        name: 'azureClientId'
        value: managedIdentityClientId
      }
    ]
    scopes: appScopes
  }
}

In our code, we can now retrieve the Secret Key using the Dapr APIs (the API actually is in the sidecar which does the heavy lifting, talking to Azure Key vault and returning the secret). Worth mentioning that our Backend service will have its own managed identity which will be used to retrieve the key vault secrets, therefore, we are not leaving any secrets in our code. Very clean and elegant.

With that, we can now create our container image for this backend service.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["JobListener.csproj", "."]
RUN dotnet restore "./JobListener.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "JobListener.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "JobListener.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "JobListener.dll"]

We need to have a Container Registry to keep these images. Let’s create one on Azure. Here’s the Bicep for Azure Container Registry (also uses Managed Identity):

resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {
  name: registryName
  location: location
  sku: {
    name: skuName
  }
  properties: {
    adminUserEnabled: adminUserEnabled
  }
}

Let’s assign AcrPull role to the user assigned Identity that we created in the part 1 of this series.

@description('This is the built-in AcrPull role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles')
var acrPushRoleDefinitionId  = '8311e382-0749-4cb8-b61a-304f252e45ec'

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

resource acrPushRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: containerRegistry
  name: guid(containerRegistry.name, 'acr-push')
  properties: {
    roleDefinitionId: acrPushRoleDefinition.id
    principalId: userAssignedIdentityPrincipalId
  }
}

Time to deploy this service as Azure Container App. How we will do that – you ask. Of course, using Bicep again. But this time we will create a reusable module in Bicep for every container that we want to create App for.

param containerAppName string
param location string = resourceGroup().location
param revisionSuffix string = uniqueString(resourceGroup().id)
param environmentName string 
param containerImage string
param containerPort int
param isExternalIngress bool
param containerRegistry string
param containerRegistryUsername string
param isPrivateRegistry bool
param enableIngress bool 
@secure()
param registryPassword string
param useManagedIdentityForImagePull bool = false
param minReplicas int = 0
param secrets array = []
param env array = []
@allowed([
  'Single'
  'Multiple'
])
param revisionMode string = 'Single'
param hasIdentity bool
param userAssignedIdentityName string

var sanitizedRevisionSuffix = substring(revisionSuffix, 0, 10)

resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
  name: userAssignedIdentityName
}

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

resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  identity: hasIdentity ? {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${uami.id}': {}
    }
  } : null
  properties: {
    managedEnvironmentId: environment.id    
    configuration: {
      activeRevisionsMode: revisionMode
      secrets: secrets      
      registries: isPrivateRegistry ? [
        {
          server: containerRegistry
          identity: useManagedIdentityForImagePull ? uami.id : null
          username: useManagedIdentityForImagePull ? null : containerRegistryUsername
          passwordSecretRef: useManagedIdentityForImagePull ? null : registryPassword
        }
      ] : null
      ingress: enableIngress ? {
        external: isExternalIngress
        targetPort: containerPort
        transport: 'auto'
        traffic: [
          {
            latestRevision: true
            weight: 100
          }
        ]
      } : null
      dapr: {
        enabled: true
        appPort: containerPort
        appId: containerAppName
      }
    }
    template: {
      revisionSuffix: sanitizedRevisionSuffix
      containers: [
        {
          image: containerImage
          name: containerAppName
          env: env
        }
      ]
      scale: {
        minReplicas: minReplicas
        maxReplicas: 1
      }
    }
  }
}

output fqdn string = enableIngress ? containerApp.properties.configuration.ingress.fqdn : 'Ingress not enabled'

With that we can create the backend service now.

module jobListenerApp 'modules/httpApp.bicep' = {
  name: appNameJobListener
  params: {    
    location: location    
    containerAppName: appNameJobListener    
    environmentName: acaEnvName
    hasIdentity: true
    userAssignedIdentityName: uami.name
    containerImage: '${containerRegistryName}.azurecr.io/job-listener:${tagName}'
    containerRegistry: '${containerRegistryName}.azurecr.io'
    isPrivateRegistry: true
    containerRegistryUsername: ''
    registryPassword: ''    
    useManagedIdentityForImagePull: true
    containerPort: 80
    enableIngress: true    
    isExternalIngress: false
    minReplicas: 1
  }
}

To be continued

At this point, we have a backend service running as Container App that gets triggered by Dapr pubsub every time I upload any image into the storage account. It then retrieves the Computer Vision key from the Azure key vault based Dapr secrets store. It then speaks to Computer vision APIs to do the image recognition and grabs the result. Next, we will create a front-end application.

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