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
- Install terraform CLI, see https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli#install-terraform
- Check its available after installing, this should show a version, example
v1.9.8
1 | terraform version |
Sample code
- Clone DemoApi.
I built this simple CRUD(ish) app based on the Microsoft templates and adapted it to use EF Core and Postgres
- 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 | Type | User | Password | Port |
- 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 | dotnet tool install --global dotnet-ef ~ globally install the EF tooling |
- 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
- Login to Azure, select and note the subscription
1 | az login --tenant 00000000-0000-0000-0000-000000000001 |
- Copy vars.tf from
iac_example
intoiac
and update some values
subscription_id
which came from az login (this is just temp while creating the infra from local)sql_pass
, exampledfb18358-5994-4470-b75a-109981a3fcf9
, this is just a random GUIDsql_user
, examplesqldemo-admin
group_key
, examplebb8c7a39
(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 |
- Copy setup.tf and check the
hashicorp/azurerm
provider version is current, example4.14.0
- sets the cloud provider, API versions and account to connect to by subscription id
- see azurerm
- Run
terraform init
1 | terraform init ~ local initialization, and tracking with `.terraform.lock.hcl` to record the provider selections made |
- Copy the files listed below, one at a time, in order of the list, each time run
terraform plan
andterraform apply
, this will create the infrastructure needed to run the application in Azure
1 | terraform plan ~ test run, compare local tf with azure, show the difference |
- resource-group.tf
- groups resources in Azure
- see resource-group
- container-registry.tf
- registry to store and manage docker images by version
- see container_registry
- log_analytics_workspace.tf
- logging and analytics
- see log_analytics_workspace
- container_app_environment.tf
- container_app.tf
- see container_app
- mssql.tf
- see mssql_server
- see mssql_database
- see mssql_firewall_rule
Note: azurerm_postgresql_flexible_server replaces the single server used March 28, 2025
- pgsql.tf (Single Server)
The end result seen from Azure showed the resources in my Resource Group:
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
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 | ${{ vars.ACR_SERVER }} ~ demoacrbb8c7a39dev.azurecr.io |
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 | ${{ secrets.ARM_CLIENT_ID }} ~ appId |
For IAC (SQL Secrets)
These are the values manually added to to vars.tf
1 | ${{ secrets.TF_VAR_DB_PASSWORD }} ~ used with `Terraform plan` as var `sql_pass`, example dfb18358-5994-4470-b75a-109981a3fcf9 |
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
- Copy
\.github\examples\deploy-api.yaml
to\.github\workflows\deploy-api.yaml
- 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 | demoacrdev (few instance values) -> demoacrbb8c7a39dev |
- 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)
- STEPS:
- deploy-image-to-container-service
- STEPS
- Login to Azure
- Deploy to Azure Container Apps (ACA)
- STEPS
- build-and-push-image
Finding Logs in Azure
- 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 | old new |
Then look for latestRevisionName
, here kj2aaos
is the revision running in Azure
1 | old new |
- Find the container logs by Revision
- Resource group ->
demo-rg-bb8c7a39-dev
-> Container App ->demo-aca-bb8c7a39-dev
-> MonitoringLog stream
select the Revision matchingkj2aaos
, this is going to give you the running container logs, so you can see why you code code sucks :DLogs
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.
- 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
- Add storage in the portal ->
reference-rg
-> create -> search ->storage account
-> selectStorage 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
->Create
… this will take a minute or so to create and saysDeployment is in progress
Go to resource
->Data storage
->Containers
->+ Container
(New Container)- Name ->
terraform
-> Create
- Update setup.tf to include the backend
1 | terraform { |
- Migrate the terraform files from local to the new blob storage
- run
terraform plan
, this should error withError: Backend initialization required, please run "terraform init"
because we added a new backend - run
terraform init
, this will askDo you want to copy existing state to the new backend?
, typeyes
Locally terraform.tfstate
should now be blank.
- 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
- STEPS:
- terraform-deploy
- Add a tag change, commit & push
1 | resource "azurerm_resource_group" "demo-rg" { |
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 | Error: updating Container App (Subscription: "00000000-0000-0000-0000-000000000002" |
Deactivate revision
1 | az containerapp revision deactivate --revision demo-aca-bb8c7a39-dev--os0hfry --resource-group demo-rg-bb8c7a39-dev |
Also check if you actuall need revision_mode=Multiple
MissingSubscriptionRegistration
While trying to add azurerm_container_app_environment it shat the bed
1 | azurerm_container_app_environment.demo-cae: Creating... |
Here https://aka.ms/rps-not-found helped us out and showed the commands to run.
- Query the provide
1 | az provider list --query "[?namespace=='Microsoft.App']" --output table |
Should output: NotRegistered
1 | Namespace RegistrationState RegistrationPolicy |
- 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 |
- Check the status
1 | az provider list --query "[?namespace=='Microsoft.App']" --output table |
Registering
1 | Namespace RegistrationState RegistrationPolicy |
Registered
1 | Namespace RegistrationState RegistrationPolicy |
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
- https://www.npgsql.org/efcore/?tabs=onconfiguring
- https://datacenters.microsoft.com/globe/explore
- https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors
- https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-register-resource-provider
- https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?tabs=azure-cli
Technologies Used
- Azure Container Registry (Build store and manage container images)
- Azure Container Apps (Built on K8s, is Serverless & Fully managed by Azure)
- Azure SQL Database (Managed PaaS database engine)
- Azure Log Analytics Workspace (Observability and Analytics)
- Github Actions
- Terraform (Consistently build infrastructure using automation)
- .NET SDK
Costings
- Database
- Container App
- Container Registry
- Storage