Last time, I wrote the backend service (JobListener
) which gets triggered by the Dapr pubsub whenever an image is uploaded into the designated blob container in the storage account. It then uses Computer Vision APIs to do the image recognition. Today we will write a front-end application which will allow users to upload images from the user interface and store it into the blob container. Also, the backend will use SignalR to notify the user at his/her browser about the image recognition outcome.
Creating the Azure SignalR
Here’s the bicep component that provisions the SignalR resource.
@description('Name of the Azure SignalR service')
param signalRName string
param location string = resourceGroup().location
param keyVaultName string
param signalRKeyName string
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = {
name: keyVaultName
}
resource signalR 'Microsoft.SignalRService/SignalR@2018-03-01-preview' = {
name: signalRName
location: location
sku: {
name: 'Free_F1'
tier: 'Free'
size: 'F1'
capacity: 1
}
properties: {
}
}
module signalRSecret 'kvSecret.bicep' = {
name: '${signalRName}-deploy-secrets'
params: {
keyVaultName: keyVault.name
secretName: signalRKeyName
secretValue: signalR.listKeys().primaryConnectionString
}
}
With SignalR provisioned, I can send the image recognition result near-real-time to the end user. You can see the code for the front-end in GitHub.
Continuous Deployment
Now that the application is functional, I wanted to write the GitHub actions to continuously deploy the code to the cloud. I decided to create one designated action that will only provision the infrastructure on Azure. And a separate action that will build the container images and deploy the code.
Let’s start with the first action:
Workload Identity
GitHub action need to talk to Azure and it requires authentication. I will be using Workload federated identity -which is a very neat way to deploy code from GitHub without any secrets spread outside of Azure.
First, we need to configure a trust relationship between your app in Azure AD and a GitHub repo in the Azure portal or using Microsoft Graph. Then configure a GitHub Actions workflow to get an access token from.

With this trust relation configured, we only need to configure Tenant ID, client ID (the app ID) and Subscription ID. But no client secrets or certificates are needed. That is very neat.

With this, we are ready to create the action workflows.
Infrastructure workflow
name: Infrastructure
on:
push:
branches: [ main ]
permissions:
id-token: write
contents: read
env:
AZURE_RESOURCE_GROUP: xeniel
AZURE_LOCATION: westeurope
AZURE_MANAGED_BY: "moim.hossain@microsoft.com"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: OIDC Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: false
- name: Provision Resource Group
uses: Azure/cli@v1
with:
inlineScript: az group create --name ${{ env.AZURE_RESOURCE_GROUP }} --location ${{ env.AZURE_LOCATION }} --tags Purpose=Demo Production=NO --managed-by ${{ env.AZURE_MANAGED_BY }}
- name: Deploy Bicep Template
uses: Azure/arm-deploy@main
with:
scope: resourcegroup
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
resourceGroupName: ${{ env.AZURE_RESOURCE_GROUP }}
template: ./Infrastructure/main.bicep
- name: Azure logout
run: az logout
Nice, this gives us the required infrastructure provisioned and ready for our container image deployment.
Container workflow
Let’s create a pipeline that builds and deploys the images to the Azure Container App environment.
name: Deployment
on:
push:
branches: [ main ]
permissions:
id-token: write
contents: read
env:
AZURE_RESOURCE_GROUP: xeniel
AZURE_LOCATION: westeurope
AZURE_MANAGED_BY: "moim.hossain@microsoft.com"
jobs:
build-container-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: tenhaus/get-release-or-tag@v2
id: tag
- name: OIDC Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: false
- name: Building container images
run: ${PWD}/CognitiveDemo/build.sh $ImageTag $RegistryName
env:
ImageTag: ${{ steps.tag.outputs.tag }}
RegistryName: "xenielscontainerregistry.azurecr.io"
- name: Azure logout
run: az logout
deploy-containers:
needs: build-container-images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: tenhaus/get-release-or-tag@v2
id: tag
- name: OIDC Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: false
- name: Deploy Bicep Template
uses: Azure/arm-deploy@main
with:
scope: resourcegroup
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
resourceGroupName: ${{ env.AZURE_RESOURCE_GROUP }}
template: ./Infrastructure/application.bicep
parameters: 'tagName=${{ steps.tag.outputs.tag }}'
- name: Azure logout
run: az logout
This gives us a multi-stage pipeline where the first stage builds the images and pushes to the container registry and the second stage does the deployment to Azure container apps.

That’s it for today, it gives us the application URI that we can use from browser and do image recognition.
To be continued
Next, I would like to use the revision capability of Azure container apps to gradually release a new version and use traffic splitting feature.
The entire source code can be found in GitHub – if you want to take a look.
Thanks for reading.