Deploying .NET Applications To Azure

Overview of my notes for Deploying .NET Applications To Azure, the idea is to be able to follow them again later and apply to other deployments.

For a guided tutorial I highly recommend Dometrain - From Zero to Hero: Deploying .NET Applications to Azure by Mohamad Lawand

Example container app ingress : https://demo-aca-bb8c7a39-dev.bluefield-f45026b3.australiaeast.azurecontainerapps.io/swagger/index.html, so demo-aca-bb8c7a39-dev comes from container_app.tf

Local Setup

Terraform CLI

  1. Install terraform CLI, see https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli#install-terraform
  2. Check its available after installing, this should show a version, example v1.9.8
1
terraform version

Sample code

  1. Clone DemoApi.

I built this simple CRUD(ish) app based on the Microsoft templates and adapted it to use EF Core and Postgres

  1. Run the database locally using docker compose, there are 2 dbs because the tutorial used MS SQL but PGSQL is cheaper and the one that I would use for actual deployments.
1
docker compose up

The default passwords are

1
2
3
4
Type        | User        | Password                             | Port
------------------------------------------------------------------------
MS SQL | sa | 00000000-0000-0000-0000-919333ac7aaf | 1433
PostgreSQL | postgres | 00000000-0000-0000-0000-919333ac7aaf | 5432
  1. Run the API, its .NET 8.0 so still has Swagger. The DB migrations should run automagically when the app starts.
1
dotnet run

Swagger is enabled for all environments so I can use the Swagger UI when deployed to Azure, normally this is only for local development or demo purposes.

The manual steps to run the migrations when testing were as follows

1
2
3
4
5
dotnet tool install --global dotnet-ef                                                     ~ globally install the EF tooling
dotnet build ~ build the solution

dotnet ef migrations add Initial_Migration --project .\src\DemoApi\DemoApi.csproj ~ add a migration based on the application models
dotnet ef database update --project .\src\DemoApi\DemoApi.csproj ~ apply the migration
  1. Check the app can locally insert and read data from the database

Create Azure Infrastructure With Terraform

You will need to know your tenant id, its just a GUID like 00000000-0000-0000-0000-000000000001

  1. Login to Azure, select and note the subscription
1
az login --tenant 00000000-0000-0000-0000-000000000001
  1. Copy vars.tf from iac_example into iac and update some values
  • subscription_id which came from az login (this is just temp while creating the infra from local)
  • sql_pass, example dfb18358-5994-4470-b75a-109981a3fcf9, this is just a random GUID
  • sql_user, example sqldemo-admin
  • group_key, example bb8c7a39 (Some Azure resources need to be unique, while testing its helpful to use the start of a random GUID)

This CLI command was helpful when I was trying to understand the regions which I stored as location in vars.tf

1
az account list-locations -o table      ~ list of Azure regions
  1. Copy setup.tf and check the hashicorp/azurerm provider version is current, example 4.14.0
  • sets the cloud provider, API versions and account to connect to by subscription id
  • see azurerm
  1. Run terraform init
1
2
3
terraform init    ~ local initialization, and tracking with `.terraform.lock.hcl` to record the provider selections made
~ Per the docs: Include this file in your version control repository
~ so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future.
  1. Copy the files listed below, one at a time, in order of the list, each time run terraform plan and terraform apply, this will create the infrastructure needed to run the application in Azure
1
2
3
4
terraform plan    ~ test run, compare local tf with azure, show the difference

terraform apply ~ execute, should create the resources described in tf plan
~ this will then create `terraform.tfstate` which is used to compare local to Azure

Note: azurerm_postgresql_flexible_server replaces the single server used March 28, 2025

The end result seen from Azure showed the resources in my Resource Group:

Resources created in Azure

You can also view the groups from the CLI

1
az group list --output table            ~ list resource groups

Test Connect To SQL

The username and DNS shown here was just for the demo, best practice is never to commit or share any secrets.

I then added my own IP address to the firewall by navigating to Resource Group (demo-rg-bb8c7a39-dev) -> selected the SQL server (demo-sql-bb8c7a39-dev) -> Security -> Networking -> Firewall rules -> Add your client IPv4 address (xxx.xxx.xxx.xxx) -> Azure automagically filled mine in -> Rule name Carl home IP -> Save. Your IP wont be static so will probs need an update later.

I then needed to get the server address for SQL in Azure by navigating to SQL server (demo-sql-bb8c7a39-dev), my server name was demo-sql-bb8c7a39-dev.database.windows.net, so the suffix comes from mssql.tf where we set the server name

Based on the sql_pass/user values in vars.tf I build then connected using DBeaver

Test sql connection

Create Github Secrets and Variables

Github Settings -> Secrets and variables -> actions

  • Secrets tab -> Repository secrets -> New repository secret
  • Variables tab -> Repository variables -> New repository variable

For Azure Container Registry

To get these values navigate to Container Registry demoacrbb8c7a39dev -> Settings -> Access keys

1
2
3
${{ vars.ACR_SERVER }}              ~ demoacrbb8c7a39dev.azurecr.io
${{ secrets.ACR_USER }} ~ demoacrbb8c7a39dev
${{ secrets.ACR_PASSWORD }} ~ 000000000000000000000/000000000000000000000000000000000000

For Azure Container App

Create a Service Principle and configure its access to Azure resources, here we copy the entire JSON object and save as AZ_CREDENTIALS github secret

1
az ad sp create-for-rbac --name github-auth --role contributor --scopes /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/demo-rg-bb8c7a39-dev --json-auth --output json
1
${{ secrets.AZ_CREDENTIALS }}

For IAC (RBAC)

Create a Service Principle and configure its access to Azure resources, here we copy values out of the JSON and save as individual github secrets

1
az ad sp create-for-rbac --name iac-terraform-auth
1
2
3
4
${{ secrets.ARM_CLIENT_ID }}         ~ appId
${{ secrets.ARM_CLIENT_SECRET }} ~ password
${{ secrets.ARM_SUBSCRIPTION_ID }} ~ you get this from `az login` or the Azure portal under and resource
${{ secrets.ARM_TENANT_ID }} ~ tenant

For IAC (SQL Secrets)

These are the values manually added to to vars.tf

1
2
${{ secrets.TF_VAR_DB_PASSWORD }}    ~ used with `Terraform plan` as var `sql_pass`, example dfb18358-5994-4470-b75a-109981a3fcf9
${{ secrets.TF_VAR_DB_USER }} ~ used with `Terraform plan` as var `sql_user`, example sqldemo-admin

This will look something like Server=demo-sql-bb8c7a39-dev.database.windows.net,1433; Initial Catalog=demo_db; User=sqldemo-admin; Password=dfb18358-5994-4470-b75a-109981a3fcf9; Encrypt=False;

ENSURE its added wrapped in quotes

1
${{ secrets.AZ_MSSQL_DB_CONN }}      ~ used when deploying, changes app settings ConnectionStrings__SqlServer

Github Actions - Deploy Application

  1. Copy \.github\examples\deploy-api.yaml to \.github\workflows\deploy-api.yaml
  2. Update the config values, potentially this could be from a config but this is more intentional and these values wont change for the applications life span.
1
2
3
4
demoacrdev (few instance values)     -> demoacrbb8c7a39dev
containerAppName -> demo-aca-bb8c7a39-dev
containerAppEnvironment -> demo-cae-bb8c7a39-dev
resourceGroup -> demo-rg-bb8c7a39-dev
  1. Commit it and check it deploys, example https://github.com/carlpaton/deploying-dotnet-azure/actions/runs/12670053312

Overview of deploy-api.yaml

  • JOB:
    • build-and-push-image
      • STEPS:
        • Checkout repository
        • Setup WebApi .NET
        • Configure Azure Container Registry (ACR)
        • Get commit SHA
        • Build and push image to Azure Container Registry (ACR)
    • deploy-image-to-container-service
      • STEPS
        • Login to Azure
        • Deploy to Azure Container Apps (ACA)

Finding Logs in Azure

  1. Find the Revision
  • Resource group -> demo-rg-bb8c7a39-dev -> Container App -> demo-aca-bb8c7a39-dev -> Activity Log -> Create or Update Container App -> Create or Update Container App -> Change history -> properties.provisioningState

Look for image, here the version 965d7c3 is my commit Sha so I can see what code I changed

1
2
old                                                   new
"image": "demoacrdev.azurecr.io/demoacrdev:478d482" "image": "demoacrdev.azurecr.io/demoacrdev:965d7c3"

Then look for latestRevisionName, here kj2aaos is the revision running in Azure

1
2
old                                                   new
"latestRevisionName": "demo-acadev--gw4qzwk" "latestRevisionName": "demo-acadev--kj2aaos"
  1. Find the container logs by Revision
  • Resource group -> demo-rg-bb8c7a39-dev -> Container App -> demo-aca-bb8c7a39-dev -> Monitoring
    • Log stream select the Revision matching kj2aaos, this is going to give you the running container logs, so you can see why you code code sucks :D
    • Logs you can write and execute your own log queries, this is helpful when you have heaps of logs and need to diagnose problems.

Github Actions - Deploy IAC

We need to migrate the TF state, terraform.tfstate and terraform.tfstate.backup from being stored locally to being stored in the cloud.

  1. Create a new resource group in Azure using the portal -> Resource groups -> Create
  • Name -> reference-rg
  • Region -> (Asia Pacific) Australia East
  • Tags -> environment=dev, source=azure-portal, group_key=bb8c7a39
  • Review and create -> Create
  1. Add storage in the portal -> reference-rg -> create -> search -> storage account -> select Storage account by Microsoft | Azure Service
  • Plan -> Storage account -> Create
  • Storage account name -> demoiacbb8c7a39
  • Region -> (Asia Pacific) Australia East
  • Primary service -> Azure Blob Storage or Azure Data Lake Storage Gen 2
  • Performance -> Standard
  • Redundancy -> Locally-redundant storage (LRS)
  • Review and create -> Createthis will take a minute or so to create and says Deployment is in progress
  • Go to resource -> Data storage -> Containers -> + Container (New Container)
  • Name -> terraform -> Create
  1. Update setup.tf to include the backend
1
2
3
4
5
6
7
8
9
10
11
terraform {
...

backend "azurerm" {
resource_group_name = "reference-rg"
storage_account_name = "demoiacbb8c7a39"
container_name = "terraform"
key = "terraform.tfstate"
}

}
  1. Migrate the terraform files from local to the new blob storage
  • run terraform plan, this should error with Error: Backend initialization required, please run "terraform init" because we added a new backend
  • run terraform init, this will ask Do you want to copy existing state to the new backend?, type yes

tfstate was copied to blob storage

Locally terraform.tfstate should now be blank.

  1. Copy \.github\examples\deploy-iac.yaml to \.github\workflows\deploy-iac.yaml, no changes needed, commit and push.

Overview of deploy-iac.yaml

  • JOB:
    • terraform-deploy
      • STEPS:
        • Checkout repository
        • Login to Azure
        • Terraform install
        • Terraform initialization
        • Terraform validate
        • Terraform plan
        • Terraform apply
        • Notify failure
  1. Add a tag change, commit & push
1
2
3
4
5
6
7
8
resource "azurerm_resource_group" "demo-rg" {
...

tags = {
...
testtag = "testtag1"
}
}

If there are state lock issues run like Error: Error acquiring the state lock -> Error message: state blob is already locked then this can be unlocked from the CLI where 092ff1c9-467a-39a4-d0cf-b72ae52e5805 is the lock ID

1
terraform force-unlock -force 092ff1c9-467a-39a4-d0cf-b72ae52e5805

This should now deploy IAC changes.

Errors

ContainerAppRegistryInUse

1
2
3
Error: updating Container App (Subscription: "00000000-0000-0000-0000-000000000002"
│ Resource Group Name: "demo-rg-bb8c7a39-dev"
│ Container App Name: "demo-aca-bb8c7a39-dev"): performing CreateOrUpdate: unexpected status 409 (409 Conflict) with error: ContainerAppRegistryInUse: Container App 'demo-aca-bb8c7a39-dev' has active revisions pulling images from the registries you are trying to delete. Please add back registries demoacrbb8c7a39dev.azurecr.io or deactive the revisions: demo-aca-bb8c7a39-dev--3rq25p3,demo-aca-bb8c7a39-dev--6x8q5l4,demo-aca-bb8c7a39-dev--c9gj9sn,demo-aca-bb8c7a39-dev--ffvr4sc,demo-aca-bb8c7a39-dev--indydxx,demo-aca-bb8c7a39-dev--os0hfry,demo-aca-bb8c7a39-dev--qri6938,demo-aca-bb8c7a39-dev--ttk7uqx pulling images from these registries.

Deactivate revision

1
az containerapp revision deactivate --revision demo-aca-bb8c7a39-dev--os0hfry --resource-group demo-rg-bb8c7a39-dev

multiple revisions

Also check if you actuall need revision_mode=Multiple

MissingSubscriptionRegistration

While trying to add azurerm_container_app_environment it shat the bed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
azurerm_container_app_environment.demo-cae: Creating...

│ Error: creating Managed Environment (Subscription: "00000000-0000-0000-0000-000000000002"
│ Resource Group Name: "demo-rg"
│ Managed Environment Name: "demo-cae-dev"): performing CreateOrUpdate: unexpected status 409 (409 Conflict) with error: MissingSubscriptionRegistration: The subscription is not registered to use namespace 'Microsoft.App'. See https://aka.ms/rps-not-found for how to register subscriptions.

│ with azurerm_container_app_environment.demo-cae,
│ on container_app_environment.tf line 1, in resource "azurerm_container_app_environment" "demo-cae":
│ 1: resource "azurerm_container_app_environment" "demo-cae" {

│ creating Managed Environment (Subscription: "00000000-0000-0000-0000-000000000002"
│ Resource Group Name: "demo-rg"
│ Managed Environment Name: "demo-cae-dev"): performing CreateOrUpdate: unexpected status 409 (409 Conflict) with error: MissingSubscriptionRegistration: The subscription is not registered to use namespace 'Microsoft.App'. See
│ https://aka.ms/rps-not-found for how to register subscriptions.

Here https://aka.ms/rps-not-found helped us out and showed the commands to run.

  1. Query the provide
1
az provider list --query "[?namespace=='Microsoft.App']" --output table

Should output: NotRegistered

1
2
3
Namespace      RegistrationState    RegistrationPolicy
------------- ------------------- --------------------
Microsoft.App NotRegistered RegistrationRequired
  1. Register which should output Registering is still on-going. You can monitor using 'az provider show -n Microsoft.App'
1
az provider register --namespace Microsoft.App
  1. Check the status
1
az provider list --query "[?namespace=='Microsoft.App']" --output table

Registering

1
2
3
Namespace      RegistrationState    RegistrationPolicy
------------- ------------------- --------------------
Microsoft.App Registering RegistrationRequired

Registered

1
2
3
Namespace      RegistrationState    RegistrationPolicy
------------- ------------------- --------------------
Microsoft.App Registered RegistrationRequired

AuthorizationFailed

1
│ Error: Failed to get existing workspaces: Error retrieving keys for Storage Account "demoiacbb8c7a39": storage.AccountsClient#ListKeys: Failure responding to request: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. Status=403 Code="AuthorizationFailed" Message="The client 'a22c2ac0-df8d-4b13-9e6c-d56b2c13ebf6' with object id 'a22c2ac0-df8d-4b13-9e6c-d56b2c13ebf6' does not have authorization to perform action 'Microsoft.Storage/storageAccounts/listKeys/action' over scope '/subscriptions/***/resourceGroups/reference-rg/providers/Microsoft.Storage/storageAccounts/demoiacbb8c7a39' or the scope is invalid. If access was recently granted, please refresh your credentials."

We need to apply a role to this client

1
az role assignment create --assignee a22c2ac0-df8d-4b13-9e6c-d56b2c13ebf6 --role Contributor --scope /subscriptions/00000000-0000-0000-0000-000000000002

References

Technologies Used

Costings