DEV Community

Cover image for Procedures for Deploying .NET 9 App to Azure Kubernetes Service (AKS)
Nelson Akpa
Nelson Akpa

Posted on

Procedures for Deploying .NET 9 App to Azure Kubernetes Service (AKS)

🚀 Table of Contents

  1. Introduction
  2. Required software installations and verification.
  3. Create the .NET Application
  4. Containerize Your Application
  5. Set Up Azure Infrastructure
  6. Create Kubernetes Configuration
  7. Set Up GitHub Actions CI/CD
  8. Access Your Deployed Application
  9. Test Continuous Deployment
  10. Clean Up Resources (Optional)
  11. Conclusion.

🚀 1. Introduction
Deploying a .NET 9 application to Azure Kubernetes Service (AKS) unlocks the full potential of cloud-native architecture with cutting-edge performance and scalability. This article outlines a streamlined deployment workflow—from containerizing your .NET 9 codebase to orchestrating it within AKS clusters. Whether you're modernizing legacy systems or launching new microservices, you'll gain practical insights to build, ship, and scale with confidence in the Azure ecosystem.
process
🚀 2. Required software installations and verification

  • Visual Studio Code - Download here
  • NET 8 SDK - Download here
  • Git - Download here
  • Docker Desktop - Download here
  • Important: Start Docker Desktop after installation
  • Azure CLI - Download here
  • kubectl - Download here
  • GitHub CLI (Optional) - Download here vrd 🚀 3.0 CREATE THE .NET APPLICATION 3.1 Set Up Project Structure First, open your VSCode and set your files to "autosave. " Then, navigate to the file on your laptop and create a new folder named "DotNetApp." bok nopl bok Create the new Folder DotNetApp or choose any name of your choice bab Open the new Folder bop Open the terminal, click on the top screen's view section, and then select terminal. hop Run the following code in VSCode as shown in the screenshots: mkdir weather-app-demo cd weather-app-demo mkdir WeatherApp cd WeatherApp deop 3.2 Initialize .NET Project Run dotnet new webapi -minimal bapo 3.3. Add Required Dependencies Run the following separately: a. dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks pokl b. dotnet add package Swashbuckle.AspNetCore gvae 3.4. Create the Application Code Replace the contents of Program.cs with the code below; using Swashbuckle.AspNetCore.SwaggerUI;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddHealthChecks();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure Kestrel to listen on port 8080 (required for containers)
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080);
});

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather API v1");
c.RoutePrefix = "swagger";
});
}

// Define API endpoints
app.MapGet("/", () => new
{
Message = "Welcome to the Weather App!",
Version = "1.0.0",
Environment = app.Environment.EnvironmentName,
Timestamp = DateTime.UtcNow
})
.WithName("GetWelcome")
.WithTags("General");

app.MapGet("/weather", () =>
{
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

var forecast = Enumerable.Range(1, 5).Select(index => new
{
    Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
    TemperatureC = Random.Shared.Next(-20, 55),
    Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.Select(temp => new
{
    temp.Date,
    temp.TemperatureC,
    TemperatureF = 32 + (int)(temp.TemperatureC / 0.5556),
    temp.Summary
});

return forecast;
Enter fullscreen mode Exit fullscreen mode

})
.WithName("GetWeatherForecast")
.WithTags("Weather");

// Health check endpoint (required for Kubernetes)
app.MapHealthChecks("/health")
.WithTags("Health");

app.Run();
dap
3.5. Test Locally
Run dotnet run
kolu
Open your browser and test these endpoints:
http://localhost:8080/ - Welcome message
pol
http://localhost:8080/weather - Weather forecast
dam
http://localhost:8080/swagger - API documentation
bim
http://localhost:8080/health - Health check
ban
Press Ctrl+C to stop the application.
deb
🚀 4. CONTAINERIZE YOUR APPLICATION
4.1. Create a Dockerfile file (no extension) in the Weatherapp folder. Paste the code below into the file;

Multi-stage build for optimized image size

Build stage

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

âś… Copy csproj from current dir (Dockerfile context = Weatherapp/)

COPY ["Weatherapp.csproj", "./"]
RUN dotnet restore "Weatherapp.csproj"

Copy source code and build

COPY . .
RUN dotnet build "Weatherapp.csproj" -c $BUILD_CONFIGURATION -o /app/build

Publish stage

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "Weatherapp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

Runtime stage

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production

Install curl

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

Final stage

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WeatherApp.dll"]
bok
4.2 Create .dockerignore
Create a .dockerignore file to exclude unnecessary files. Paste the file below into the Dockerignore file.

Build outputs

bin/
obj/
out/

IDE files

.vs/
.vscode/
*.user
*.suo

OS files

.DS_Store
Thumbs.db

Git

.git/
.gitignore

Documentation

README.md
*.md

Docker files

Dockerfile
Dockerfile.*
.dockerignore

Logs

.log
logs/
bad
**4.3 Build and Test Container
*
Run the following code;

Build the Docker image

docker build -t weather-app: local .
bop

Run the container

docker run -d -p 8080:8080 --name weather-test weather-app:local
bol

Test the containerized app

curl http://localhost:8080/
cop
curl http://localhost:8080/weather
top
# Clean up
Run docker stop weather-test
kop
docker rm weather-test
gok
5. SET UP AZURE INFRASTRUCTURE
Run az login
This connects to your Azure account and opens up a list of subscriptions.
VED
5.2 Select your subscription
Choose your subscription from the list of others, or simply run the following;

List available subscriptions

az account list --output table

Set the subscription you want to use

az account set --subscription "Your-Subscription-Name"
5.3. Create a resource group
Run the following code;
az group create --name student-demo --location eastus
sed
5.4. Create a Container Registry

Replace 'studentdemo2024acr' with a unique name (add your initials/year)

az acr create \
--resource-group student-demo \
--name studentdemo2074acr \
--sku Basic

5.5. Build and Push Image Using ACR
From the folder that contains your Dockerfile, run:
az acr build \
--registry studentdemo2074acr \
--image weather-app:latest \
.
lop
kol
5.6 Create AKS Cluster with ACR integration.
Run this code
az aks create \
--resource-group student-demo \
--name student-aks-cluster \
--node-count 1 \
--node-vm-size Standard_B2s \
--attach-acr studentdemo2074acr \
--enable-managed-identity \
--generate-ssh-keys
nom
5.7. Connect To Your Cluster

Download cluster credentials

az aks get-credentials \
--resource-group student-demo \
--name student-aks-cluster
okl

# Verify connection
kubectl get nodes
gad
5.8. Verify ACR Integration (Optional)

Get the managed identity client ID

Run the code;
CLIENT_ID=$(az aks show \
--resource-group student-demo \
--name student-aks-cluster \
--query "identityProfile.kubeletidentity.clientId" \
--output tsv)

echo "Kubelet Client ID: $CLIENT_ID"
huo
# Verify the role assignment exists
az role assignment list \
--assignee $CLIENT_ID \
--scope $(az acr show --name studentdemo2074acr --query id --output tsv) \
--output table
poj

You should see an AcrPull role assignment. If not, create it manually:

Only if the role assignment doesn't exist

Run;
az role assignment create \
--assignee $CLIENT_ID \
--role AcrPull \
--scope $(az acr show --name studentdemo2074acr --query id --output tsv)
6. CREATE KUBERNETES CONFIGURATION
6.1. Set Up Kubernetes Manifests Directory
Run the following code;
cd .. (# Back to weather-app-demo folder)
kop
mkdir k8s
cd k8s
kol
6.2. Create Deployment Configuration
Create deployment.yaml
paste the code below into the file;
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-app
namespace: default
labels:
app: weather-app
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: weather-app
template:
metadata:
labels:
app: weather-app
version: v1
spec:
imagePullSecrets:
- name: acr-pull-secret
containers:
- name: weather-app
image: studentdemo2074acr.azurecr.io/weather-app:

      imagePullPolicy: Always
      ports:
        - name: http
          containerPort: 8080
          protocol: TCP
      env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
      resources:
        requests:
          memory: "128Mi"
          cpu: "100m"
        limits:
          memory: "512Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /health
          port: http
        initialDelaySeconds: 30
        periodSeconds: 10
        timeoutSeconds: 5
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /health
          port: http
        initialDelaySeconds: 10
        periodSeconds: 5
        timeoutSeconds: 3
        failureThreshold: 3
      startupProbe:
        httpGet:
          path: /health
          port: http
        initialDelaySeconds: 15
        periodSeconds: 5
        timeoutSeconds: 3
        failureThreshold: 30
Enter fullscreen mode Exit fullscreen mode

pok
Note: There's no need for imagePullSecrets because AKS with --attach-acr handles authentication automatically!
6.3. Create Service Configuration
Create service.yaml and paste the code below into the file.
apiVersion: v1
kind: Service
metadata:
name: weather-app-service
labels:
app: weather-app
spec:
type: LoadBalancer
selector:
app: weather-app
ports:

  • name: http port: 80 targetPort: 8080 protocol: TCP top 6.4 Deploy to Kubernetes Run the codes individually. # Apply Kubernetes manifests kubectl apply -f deployment.yaml dam kubectl apply -f service.yml bap # Check deployment status kubectl get deployments dat kubectl get pods hop kubectl get services get

Watch pods until they're running

kubectl get pods --watch
good
6.5. Troubleshoot if Needed
If you see ImagePullBackOff:

Check pod events

kubectl describe pod $(kubectl get pods -l app=weather-app -o jsonpath='{.items[0].metadata.name}')

Check if the image exists in ACR

az acr repository show-tags --name studentdemo2024acr --repository weather-app --output table

Force a new pull

kubectl rollout restart deployment/weather-app
7.0. SET UP GITHUB ACTIONS CI/CD
7.1. Initialize Git Repository
Run the following codes
cd .. (# Back to weather-app-demo folder)
best

Initialize Git repository

git init
git add .
git commit -m "Initial commit: Weather App with Docker and Kubernetes"
great
7.2. Create Azure Service Principal

Get your subscription ID

SUBSCRIPTION_ID=$(az account show --query id --output tsv)
echo "Subscription ID: $SUBSCRIPTION_ID"
dec

Create service principal with contributor role

az ad sp create-for-rbac \
--name "weather-app-github-sp" \
--role contributor \
--scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/student-demo \
--sdk-auth
gaj
Important: Copy the entire JSON output in the red box - you'll need it for GitHub secrets!
7.3. Create GitHub Repository

Create GitHub repository (using GitHub CLI)

gh auth login # Follow the prompts
GEM
GOP
BAD
gh repo create weather-app-demo --public --source=. --push
AMP
Alternative: Create the repository manually on GitHub.com and push your code.
7.4. Configure GitHub Secrets
Navigate to your GitHub repository:

Go to Settings → Secrets and variables → Actions
Click New repository secret and add these secrets:
NOK
Navigate to secrets and variables
gpo
pok
ball

Secret Name
Value
AZURE_CREDENTIALS
The JSON output from step 5.2
ACR_NAME
studentdemo2074acr (your ACR name)
RESOURCE_GROUP
student-demo
CLUSTER_NAME
student-aks-cluster
nop
okop
bot
total
7.5. Create GitHub Workflow
Create the workflow directory and file by running the code below;
mkdir -p .github/workflows
touch .github/workflows/deploy.yml
name: Build and Deploy to AKS

on:
push:
branches: [main]
workflow_run:
workflows: ["CI - Build and Test"]
types: [completed]
branches: [main]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }}

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Setup .NET 9.0
    uses: actions/setup-dotnet@v4
    with:
      dotnet-version: '9.0.x'

  - name: Restore dependencies
    run: dotnet restore WeatherApp/WeatherApp.csproj

  - name: Build application
    run: dotnet build WeatherApp/WeatherApp.csproj --configuration Release --no-restore

  - name: Run tests
    run: dotnet test WeatherApp/WeatherApp.csproj --no-build --verbosity normal

  - name: Azure Login
    uses: azure/login@v1
    with:
      creds: ${{ secrets.AZURE_CREDENTIALS }}

  - name: Login to Azure Container Registry
    run: |
      az acr login --name ${{ secrets.ACR_NAME }}

  - name: Set IMAGE_NAME environment variable
    run: echo "IMAGE_NAME=weather-app" >> $GITHUB_ENV

  - name: Set NAMESPACE environment variable
    run: echo "NAMESPACE=default" >> $GITHUB_ENV

  - name: Build and push Docker image to ACR
    run: |
      IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/$IMAGE_NAME:${{ github.sha }}
      docker build -t $IMAGE_TAG WeatherApp/
      docker push $IMAGE_TAG
      echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV

  - name: Test Docker image
    run: |
      docker run -d -p 8080:8080 -e ASPNETCORE_URLS=http://+:8080 --name test-container $IMAGE_TAG
      sleep 10
      docker logs test-container
      curl -f http://localhost:8080/health || exit 1
      docker stop test-container
      docker rm test-container

  - name: Deploy to AKS
    if: github.ref == 'refs/heads/main'
    run: |
      # Get AKS credentials
      az aks get-credentials \
        --resource-group ${{ secrets.RESOURCE_GROUP }} \
        --name ${{ secrets.CLUSTER_NAME }} \
        --overwrite-existing

      # Check if deployment exists, if not create it
      if ! kubectl get deployment/weather-app -n default > /dev/null 2>&1; then
        echo "Creating new deployment..."
        if [ -d "k8s/" ]; then
          kubectl apply -f k8s/ -n default
        else
          echo "No k8s/ directory found. Please create Kubernetes manifests."
          exit 1
        fi
      else
        echo "Updating existing deployment..."
        kubectl set image deployment/weather-app \
          weather-app=$IMAGE_TAG \
          -n default \
          --record
      fi

      # Wait for deployment to complete
      kubectl rollout status deployment/weather-app -n default --timeout=300s

      # Get service info
      kubectl get service weather-app-service -n default || echo "Service not found"

      # Verify pods
      kubectl get pods -n default -l app=weather-app

      # Simple smoke test
      kubectl port-forward service/weather-app-service 8080:80 -n default &
      sleep 5
      curl -f http://localhost:8080/health || echo "Health check failed"
      pkill -f "kubectl port-forward"
Enter fullscreen mode Exit fullscreen mode

gate
7.6. 5.6 Deploy Your Application
git add .
git commit -m "Add GitHub Actions CI/CD pipeline"
git push origin main
baok
Monitor the deployment:
Go to your GitHub repository
Click the Actions tab
Watch your workflow run in real-time
fig
**8.0 Access Your Deployed Application

8.1 Access Your Deployed Application
Get External IP Address

Check service status

Run kubectl get service weather-app-service
bok

Wait for EXTERNAL-IP (may take 2-5 minutes)

Run kubectl get service weather-app-service --watch
nab
8.2. Test Your Live Application

Test the endpoints

curl http://YOUR-EXTERNAL-IP/
http://4.157.170.214
hod
curl http://YOUR-EXTERNAL-IP/weather
http://4.157.170.214/weather
bpl

curl http://YOUR-EXTERNAL-IP/health
http://4.157.170.214/health
gap

Or open in browser

open http://YOUR-EXTERNAL-IP/swagger # macOS
http://4.157.170.214/swagger
bap
start http://YOUR-EXTERNAL-IP/swagger # Windows
9. Test Continuous Deployment
Make a Code Change
Edit WeatherApp/Program.cs and update the welcome message:
app.MapGet("/", () => new
{
Message = "Hurray! Welcome to the Updated Weather App! 🌤️",
Version = "1.1.0",
Environment = app.Environment.EnvironmentName,
Timestamp = DateTime.UtcNow,
DeployedBy = "GitHub Actions"
})
gob
Deploy the Changes by running the following code
git add ../Weatherapp/Program.cs
git commit -m "Update welcome message and version"
DES
pok
gok
vab
# Test the endpoints on a browser
http://YOUR-EXTERNAL-IP/swagger
http://4.157.170.214
gat
10. Clean Up Resources (Optional)
Option 1: Delete Individual Resources

Delete Kubernetes resources

kubectl delete -f k8s/

Delete AKS cluster

az aks delete --resource-group student-demo --name student-aks-cluster --yes

Delete Container Registry

az acr delete --name studentdemo2024acr --yes

Option 2: Delete Everything (Recommended)

Delete the entire resource group (removes all Azure resources)

az group delete --name student-demo --yes --no-wait

🚀 Conclusion: Seamless Deployment, Scalable Success
Deploying a .NET 9 application to Azure Kubernetes Service (AKS) is no longer a daunting task—it’s a streamlined process that empowers developers to harness the full potential of cloud-native architecture. You ensure a robust, scalable, and resilient deployment pipeline by following the outlined procedures—from containerizing the app to configuring YAML manifests and leveraging Azure CLI. With AKS and .NET 9 working in tandem, your applications are primed for performance, agility, and future growth.

Top comments (0)