Deploying Angular and .NET Applications on Azure with Bicep

How to deploy Web Apps on Azure with Bicep? | Nitor Infotech
×

About the author

Pratik Balkunde
Software Engineer
Pratik Balkunde, a Software Engineer at Nitor Infotech, is currently working as a Full Stack Developer on the Gordian Project, with a strong... Read More

Software Engineering   |      10 Mar 2025   |     30 min  |

Effortless cloud management is the backbone of modern DevOps, especially when working with frameworks like Angular and .NET. Yet, navigating the complexities of traditional JSON templates often feels like untangling a knot—time-consuming and error-prone. Enter Azure Bicep—a game-changing Infrastructure as Code (IaC) tool that redefines how Azure resources are deployed. With its intuitive syntax and streamlined approach, Bicep eliminates the clutter. This way, it allows teams to focus on what matters: speed, precision, and scalability.

I’ve divided this blog into two segments. The first section introduces Azure Bicep and walks you through the essential configurations on the Azure Portal. The second section delves into deploying applications efficiently using Bicep. (Picture a seamless and practical learning experience.)

So, let’s get started!

What is Azure Bicep?

Azure Bicep is a domain-specific language (DSL) used for deploying and managing resources on Microsoft Azure. It is an Infrastructure as Code (IaC) tool designed to simplify the process of defining and deploying Azure resources. It provides a more concise, readable, and modular alternative to Azure Resource Manager (ARM) templates. These templates are written in JSON.

Here are the top three compelling reasons to choose Azure Bicep:

  • Readability and maintainability: Bicep’s syntax is much cleaner and more understandable than JSON. This makes it easier to write, read, and maintain infrastructure code.
  • Efficiency: By simplifying code and reducing complexity, it helps developers and infrastructure teams focus on solving business problems rather than wrestling with syntax.
  • Faster development and deployment: With Bicep, teams can speed up the deployment process, reduce errors, and improve the overall efficiency of managing Azure resources.

Unlock the full potential of Azure Bicep – explore its key benefits!

Before creating your Bicep file, make sure to complete some initial configurations mentioned next in the Azure Portal. Once done, you’ll be ready to write and deploy your Bicep files seamlessly.

collatral

Learn to simplify and accelerate your Azure migration.

What Azure Portal configurations are needed to deploy apps with Azure Bicep?

To deploy apps using Azure Bicep, several essential configurations need to be set up in the Azure Portal. Let’s walk through these steps (refer to the attached screenshots for each step):

1. Set Up Prerequisites

Before you start, ensure you have the following:

  • An Azure subscription
  • Access to the Azure Portal
  • Azure CLI installed (optional but recommended)

2. Create a Resource Group

A resource group acts as a container for all your related Azure resources. This is how you can create one:

  • Log in to the Azure Portal.
  • Navigate to Resource Groups > Create New.
  • Enter the following details:
    1. Subscription: Select your subscription.
    2. Resource Group Name: For example: my-resource-group.
    3. Region: Choose the region closest to your users (for example: Central India).
  • Click Review + Create, then Create.

Create a resource group

Fig: Create a resource group

3. Set Up Azure Container Registry (ACR)

ACR is essential for storing container images that will be deployed to Azure Container Apps (ACA). Here are the steps to create an ACR:

  • Go to the Azure Portal and search for Container Registry.
  • Click Create New and fill in the details:
    1. Subscription: Select your subscription.
    2. Resource Group: Select the one you created.
    3. Registry Name: For example: myacr.
    4. Location: Same as the resource group.
    5. Pricing plan: Choose basic for testing or standard/premium for production.
  • Click Review + Create, then Create.

Set up Azure Container Registry

Fig: Set up Azure Container Registry

Extra read: Learn to synchronize data in real-time using Azure.

4. Create a Container App Environment

This cannot be created directly in the Azure Portal; you’ll need to use Azure Cloud Shell instead. Here are the steps to create it:

  • Open the terminal in Azure portal.

Open the terminal in Azure Portal

Fig: Open the terminal in Azure Portal

  • Run the following commands:

1. > Check subscriptions:

az-account-list

Fig: AZ account list

Check Subscription

Fig: Check Subscription

2. > Set subscription:

AZ account set

Fig: AZ account set

Set subscription

Fig: Set subscription

3. > Create the Container App Environment by replacing the placeholders with your specific details:

replacing-placeholders

Fig: Replacing placeholders

<yourContainerAppEnvironmentName> (Unique name)
<YourResourceGroup> (Previously created)
<YourLocationasPrevious> (Previously selected)

Create Container App Environment

Fig: Create Container App Environment

5. Create Managed Identities

Follow these steps to create managed identities:

  • Search for Managed Identities in the Azure Portal.
  • Click Create and fill in the details:
    1. Subscription: Select your subscription.
    2. Resource Group: Use your existing resource group.
    3. Region: Same as the resource group.
    4. Name: Provide a unique name.
  • Click Review + Create, then Create.

Create Managed Identities

Fig: Create Managed Identities

6. Assign the User-Assigned Managed Identity to the Container Apps Environment

  • Select your Container Apps Environment in the Azure Portal.
  • Under the Settings section, select Identity.
  • Enable User-assigned managed identity.
  • Click Add and:
    1. Subscription: Select your subscription.
    2. Choose User-assigned managed identities.
    3. Add the previously created managed identity.
  • Save the changes.

assign-user-assigned-managed-identity-to-container-app-environment

Fig: Assign User-Assigned Managed Identity to Container App Environment

7. Grant ACR Pull Permissions to the User-Assigned Managed Identity

  • Go to your Azure Container Registry in the Azure Portal.
  • Navigate to Access Control (IAM) and click + Add > Add role assignment.

Grant ACR Pull Permissions to User Assigned Managed Identity 1

Fig: Grant ACR Pull Permissions to User Assigned Managed Identity 1

  • Fill in the following details:
    1. Role: Select AcrPull.
    2. Assign access to: Managed identity.
  • Click +Select Members and:
    1. Subscription: Select your subscription.
    2. Select Managed identity.
    3. Select the user-assigned managed identity you just created.
  • Click Review + Assign.

Grant ACR Pull Permissions to User Assigned Managed Identity 2

Fig: Grant ACR Pull Permissions to User Assigned Managed Identity 2

Once these configurations are complete, your Azure environment will be set for Bicep deployment. Now, let’s move on to the second part of this blog!

collatral

Learn how we helped our client modernize financial reporting with an Azure-powered solution.

How to Implement Automated Deployment with Bicep, YAML, and Azure DevOps?

First, to keep your project organized, you must adhere to the following file structure:

File Structure

Fig: File Structure

After setting up the file structure, follow these steps to streamline automated deployment leveraging Bicep, YAML, and Azure DevOps (follow the screenshots for clarity):

Step 1. Create the Environment in Azure DevOps

  • Navigate to your Azure DevOps project.
  • Go to Pipelines > Environments.
  • Click New environment.
  • Enter DEV as the environment name (ensure it matches the name in your YAML file).
  • Click Create.

Create Environment in Azure DevOps

Fig: Create Environment in Azure DevOps

Step 2. Configure Service Connections in Azure DevOps

For Service Connection 1: Docker Registry, follow these steps:

  • Go to Project Settings in Azure DevOps.
  • Select Service Connections.
  • Click New Service Connection and choose Docker Registry.

a-configure-service-connections-in-azure-devops-service-connection-2-azure-resource-manager

Fig: a. Configure Service Connections in Azure DevOps (Service Connection 1 Docker Registry)

  • Click Next and fill in the details:
    1. Registry type: Other
    2. Docker Registry: https://acracatest2.azurecr.io (replace with your registry URL)
    3. Docker ID: acracatest2 (replace with your Docker ID)
    4. Docker Password: Retrieve it from Container Registry > acracatest2 > Settings > Access Keys. Ensure that admin access is enabled if it’s not already.
    5. Service Connection Name: demo-service-acr (or a name of your choice).
    6. Security: Enable.
  • Save the configuration.

b-configure-service-connections-in-azure-devops-service-connection-1-docker-registry-v1

Fig: b. Configure Service Connections in Azure DevOps (Service Connection 1 Docker Registry)

c-configure-service-connections-in-azure-devops-service-connection-1-docker-registry

Fig: c. Configure Service Connections in Azure DevOps (Service Connection 1 Docker Registry)

For Service Connection 2: Azure Resource Manager, follow these steps:

  • Click New Service Connection and choose Azure Resource Manager.

a-configure-service-connections-in-azure-devops-service-connection-1-docker-registry

Fig: a. Configure Service Connections in Azure DevOps (Service Connection 2 Azure Resource Manager)

  • Click Next and configure as follows:
    1. Identity Type: Automatic
    2. Credential: Workload Identity Federation
    3. Scope Level: Subscription
    4. Subscription: Select your subscription.
    5. Resource Group: Select your resource group.
    6. Service Connection Name: azure-rm-demo-service (or a name of your choice).
    7. Security: Enable.
  • Save the configuration.

b-configure-service-connections-in-azure-devops-service-connection-2-azure-resource-manager

Fig: b. Configure Service Connections in Azure DevOps (Service Connection 2 Azure Resource Manager)

c-configure-service-connections-in-azure-devops-service-connection-2-azure-resource-manager

Fig: c. Configure Service Connections in Azure DevOps (Service Connection 2 Azure Resource Manager)

Step 3. Set Up and Use Bicep and YAML Files

Optional: Install Extensions

Enhance development in VS Code with these extensions:

  • Bicep
  • Azure Pipelines
  • Azure Tools
  • Azure Pipelines Tools

Then create the Dockerfile (if not already available)

Quick read: Dockerize .NET solutions and deploy via Azure DevOps CI/CD.

Here’s the Dockerfile located in the DotNetApp/ directory (make sure to test it locally by building and running the Dockerfile):

Dockerfile:

#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base

USER app

WORKDIR /app

EXPOSE 8080

EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS with-node

RUN apt-get update

RUN apt-get install curl

RUN curl -sL https://deb.nodesource.com/setup_20.x | bash

RUN apt-get -y install nodejs

RUN npm install -g @angular/cli

FROM with-node AS build

ARG BUILD_CONFIGURATION=Release

WORKDIR /src

COPY ["Demo_App.Server/Demo_App.Server.csproj", "Demo_App.Server/"]

COPY ["demo_app.client/demo_app.client.esproj", "demo_app.client/"]

RUN dotnet restore "./Demo_App.Server/Demo_App.Server.csproj"

COPY . .

WORKDIR "/src/Demo_App.Server"

RUN dotnet build "./Demo_App.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish

ARG BUILD_CONFIGURATION=Release

RUN dotnet publish "./Demo_App.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final

WORKDIR /app

COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "Demo_App.Server.dll"]

Further, create the Bicep File:

Refer to the provided main.bicep file for your deployment. Save it under the bicep/ directory.

main.bicep:

targetScope = 'resourceGroup'

@description('The location where the resources will be created.')

param location string = resourceGroup().location

@description('The name of the container app.')

param containerAppName string

@description('The container image to deploy.')

param containerAppImage string

@description('The port number for the container app.')

param containerAppPortNumber int

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

param containerAppsEnvironmentName string

@description('The name of the container registry.')

param containerRegistryName string = 'acracatest2.azurecr.io'

@description('managed identity name for the container registry.')

param userAssignedIdentityName string

@description('Tags to be applied to the resource.')

param tags object = {}

@description('managed identity name for the container registry.')

resource containerRegistryUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {

  name: userAssignedIdentityName

}

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

  name: containerAppsEnvironmentName

}

resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {

  name: containerAppName

  location: location

  tags: tags

  identity: {

    type: 'UserAssigned'

    userAssignedIdentities: {

        '${containerRegistryUserAssignedIdentity.id}': {}

    }

  }

  properties: {

    managedEnvironmentId: containerAppsEnvironment.id

    configuration: {

      registries: !empty(containerRegistryName) ? [

        {

          server: containerRegistryName

          identity: containerRegistryUserAssignedIdentity.id

        }

      ] : []

      ingress: {

        external: true

        targetPort: containerAppPortNumber

      }

    }

    template: {

      containers: [

        {

          name: containerAppName

          image: containerAppImage

        }

      ]

    }

  }

}

output containerAppId string = containerApp.id

This Bicep template deploys a containerized application with a managed identity to Azure using Azure Container Apps. It defines all necessary resources, parameters, and properties.

Here’s a detailed breakdown of the key components mentioned above:

 targetScope

Fig: targetScope

Parameters

Fig: Parameters

Resources

Fig: Resources

Output

Fig: Output

Here’s the summary of the flow:

  • Parameters let you customize location, app name, image, and more.
  • Managed Identity and Environment ensure proper permissions and setup.
  • The Container App is configured for ingress, image pulling, and environment use.
  • The Container App ID is outputted for easy reference.

Quick Info: A parameter file keeps Bicep templates flexible and environment-agnostic. This allows dynamic value application during CI/CD for consistent deployments with environment-specific configurations.

In case you’re wondering how to generate a parameter file for Bicep, here’s the process:

  1. Right-click on the main.bicep file in VS Code.
  2. Select Generate Parameter File > JSON and Required Only.
  3. This will generate a .json file with placeholders for the required parameters.

Fill in the required values in the parameter file. Here’s an example of main.dev.parameters.json for configuring deployment in the development environment:

{

  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",

  "contentVersion": "1.0.0.0",

  "parameters": {

    "containerAppName": {

      "value": "demo-aca"

    },

    "containerAppImage": {

      "value": ""

    },

    "containerAppPortNumber": {

      "value": 8080

    },

    "containerAppsEnvironmentName": {

      "value": "ACATestEnv"

    },

    "userAssignedIdentityName": {

      "value": "ACAtestMI"

    }

  }

}

Requirement list for generating a parameter file for Bicep

Fig: Requirement list for generating a parameter file for Bicep

Next, create the YAML File.

This pipeline performs the following tasks:

  1. Triggering: It triggers when changes are pushed to the main branch.
  2. Build Phase: It builds and pushes a Docker image for the application.
  3. Deployment Phase: It deploys the containerized application to the DEV environment using Bicep.

For the CI/CD configuration, refer to the azure-pipelines.yml file shown below:

trigger:

  branches:

    include:

      - main

pool:

  vmImage: ubuntu-latest

parameters:

  - name: SelectedEnvironment

    type: string

    default: DEV

    displayName: 'Select Environment'

    values:

      - DEV

variables:

  - name: acrName

    value: 'acracatest2'

  - name: azureServiceConnection

    value: 'azure-rm-demo-service'

  - name: acrServer

    value: acracatest2.azurecr.io

  - name: acrConnection

    value: 'demo-service-acr'

  - name: containerAppName

    value: 'demo-aca'

  - name: containerAppImage

    value: 'demo-app-image'

  - name: mainBicepFile

    value: bicep/main.bicep

  - ${{ if and(eq(parameters.SelectedEnvironment, 'DEV'), eq(variables['Build.SourceBranchName'], 'main')) }}:

    - name: resourceGroupName

      value: ResourceGroupACATest

    - name: csmParametersFile

      value: bicep/main.dev.parameters.json

    - name: tag

      value: $[format('{0}-{1}', '$(Build.BuildNumber)', 'dev')]

stages:

  - ${{ if and(eq(parameters.SelectedEnvironment, 'DEV'), eq(variables['Build.SourceBranchName'], 'main')) }}:

    - stage: BuildDockerDEV

      jobs:

        - job: BuildAndPushApiImage

          displayName: 'Build And Push Docker Image'

          steps:

            - task: Docker@2

              inputs:

                containerRegistry: $(acrConnection)

                repository: $(containerAppImage)

                command: 'buildandpush'

                buildContext: '$(Build.SourcesDirectory)'

                Dockerfile: '$(Build.SourcesDirectory)/Demo_App.Server/Dockerfile'

                tags: $(tag)

    - stage: DEV

      displayName: Deploy to DEV

      dependsOn: BuildDockerDEV

      condition: succeeded()

      jobs:

        - deployment: DeployInfrastructureToDEV

          environment: DEV

          displayName: 'Deploy to DEV Environment'

          strategy:

            runOnce:

              deploy:

                steps:

                  - checkout: self

                  - task: AzureResourceManagerTemplateDeployment@3

                    name: ValidateBicep

                    displayName: 'Validate Bicep'

                    inputs:

                      deploymentScope: 'Resource Group'

                      azureResourceManagerConnection: $(azureServiceConnection)

                      action: 'Create Or Update Resource Group'

                      resourceGroupName: $(resourceGroupName)

                      location: $(Location)

                      csmFile: $(mainBicepFile)

                      csmParametersFile: $(csmParametersFile)

                      overrideParameters: -containerAppImage "$(acrServer)/$(containerAppImage):$(tag)"

                      deploymentMode: 'Validation'

                  - task: AzureResourceManagerTemplateDeployment@3

                    name: DeployBicep

                    displayName: 'Deploy Infrastructure to DEV'

                    inputs:

                      deploymentScope: 'Resource Group'

                      azureResourceManagerConnection: $(azureServiceConnection)

                      action: 'Create Or Update Resource Group'

                      resourceGroupName: $(resourceGroupName)

                      location: $(Location)

                      csmFile: $(mainBicepFile)

                      csmParametersFile: $(csmParametersFile)

                      overrideParameters: -containerAppImage "$(acrServer)/$(containerAppImage):$(tag)"

                      deploymentMode: 'Incremental'

                      deploymentName: '$(containerAppName)-$(tag)'

Here’s a breakdown of the key sections above:

1. Trigger Section

This section specifies that the pipeline will be triggered whenever changes are pushed to the main branch. The pipeline will automatically run for commits in this branch.

2. Pool Section

The pool section defines the environment in which the pipeline will run. Here, it specifies the use of the ubuntu-latest virtual machine image for the build and deployment process.

3. Parameters Section

This section defines a parameter called SelectedEnvironment. This is used to specify the deployment environment (e.g., DEV). The default environment is set to DEV. However, this can be changed if needed for different environments (such as STAGING or PRODUCTION).

4. Variables Section

The variables section defines key values used throughout the pipeline, such as:

  • Azure Container Registry (ACR) connection details (acrName, acrServer)
  • The name of the container app and the image (containerAppName, containerAppImage)
  • The path to the Bicep file (mainBicepFile) for infrastructure deployment

5. Conditional Variables

This block conditionally sets additional variables (like resourceGroupName and csmParametersFile) based on the selected environment (DEV) and the branch being built (main). This ensures that only the relevant parameters are used during the deployment to the desired environment.

6. Stages Section

The stages section is divided into two stages:

  • BuildDockerDEV Stage: This stage builds the Docker image for the application and pushes it to the Azure Container Registry. It uses the Docker@2 task to build and push the image using a specified Dockerfile.
  • DEV Stage: This stage deploys the infrastructure to the DEV environment using Bicep. It includes two tasks:
    1. ValidateBicep: This task validates the Bicep deployment to ensure everything is correct before applying changes.
    2. DeployBicep: This task deploys the resources to the DEV environment, updating them incrementally.

Each task uses the AzureResourceManagerTemplateDeployment@3 task, and the container image to be deployed is dynamically passed using parameters. This is to ensure it corresponds to the build and tag generated in the earlier stages.

Key Points

Fig: Key Points 

Step 4. Set Up and Run the Pipeline

  • Navigate to Pipelines in Azure DevOps.

Set Up and Run the Pipeline 1

Fig: Set Up and Run the Pipeline 1

  • Follow these steps to create a new pipeline:
    1. Select Azure Repos Git.
    2. Choose your repository.
    3. Select Existing Azure Pipeline YAML File.
    4. Specify the branch and file path.
  • Save and run the pipeline to deploy your application.

Where is your code

Fig: Where is your code

Select repository

Fig: Select repository

Configure pipeline

Fig: Configure pipeline

Select exisiting YAML file

Fig: Select exisiting YAML file

Review pipeline YAML

Fig: Review pipeline YAML

Step 5. Post-Pipeline Execution and Verification

Once the pipeline runs successfully, verify the deployment by checking the following:

  • Pipeline Status: Here’s a screenshot confirming the successful pipeline run, with build and deployment completed without errors:

Pipeline Status

Fig: Pipeline Status

  • Azure Container Apps (ACA) Deployment: Once the pipeline is completed, navigate to your Azure portal to verify that the Container App is successfully deployed. The screenshot below shows the Container App in the Azure portal. This indicates that the application is up and running.

ACA Depolyment

Fig: ACA Depolyment

  • Application UI: Here’s a screenshot of the deployed application’s UI, showing the live app users can access after deployment:

Application UI

Fig: Application UI

All the visuals confirm the successful execution of the pipeline and the deployment of the application.

Suggestion: For a detailed overview of Azure Container Apps pricing, you can visit the official Azure pricing page.

So, just like your favorite shortcut, this blog helps you streamline CI/CD pipelines with Azure DevOps, making Angular and .NET deployments quick and hassle-free!

Loved this read? Reach us at Nitor Infotech and discover how AI-powered software product engineering can drive your growth in 2025 and beyond.

subscribe image

Subscribe to our
fortnightly newsletter!

we'll keep you in the loop with everything that's trending in the tech world.

We use cookies to ensure that we give you the best experience on our website. If you continue to use this site we will assume that you are happy with it.