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.