Cloud-native development can feel overwhelming at first—containers, Kubernetes, CI/CD pipelines, cloud providers, etc. This tutorial walks you step by step from writing a simple .NET 8 Web API to deploying it on Azure Kubernetes Service (AKS) with a fully automated GitHub Actions CI/CD pipeline.
We’ll go beyond just writing code. You’ll also learn how and why each tool fits into the bigger cloud-native picture.
Learning Objective
By completing this tutorial, you will master:
Building production-ready .NET 8 Web APIs
Containerizing applications with Docker
Deploying to Azure Kubernetes Service (AKS)
Setting up CI/CD pipelines with GitHub Actions
Applying cloud-native development best practices
Project Overview
We’ll build a Weather Forecast API and deploy it to the cloud.
Backend → .NET 8 Web API
Containerization → Docker
Cloud Platform → Azure Kubernetes Service (AKS)
Automation → GitHub Actions for CI/CD
Prerequisites
Required Software
Install these tools (in order):
⚠️ Remember: Start Docker Desktop after installation
GitHub CLI (optional)
Required Accounts
GitHub(free)
Azure (free tier with $200 credits)
Verify Installation
Run the following commands in your terminal:
dotnet --version # Shows installed .NET SDK version (should be 8.x.x)
git --version # Confirms Git is installed
docker --version # Confirms Docker CLI works
az --version # Shows Azure CLI version
kubectl version --client # Verifies kubectl client
Note: If any fail → reinstall that tool.
💻 Step 1: Create the .NET Application
1.1 Set Up Project Structure
mkdir weather-app-demo # Create root project folder
cd weather-app-demo
mkdir WeatherApp # Create app folder
cd WeatherApp
Why? → Organizing code and infra configs in separate folders makes CI/CD pipelines easier to manage.
1.2 Initialize .NET Project
dotnet new webapi -minimal
dotnet new
→ Creates a new project
webapi
→ Template for REST APIs
-minimal
→ Uses the simplified .NET 8 minimal API syntax (less boilerplate)
1.3 Add Dependencies
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
HealthChecks
→ Adds endpoints for monitoring app health (Kubernetes probes rely on this).
dotnet add package Swashbuckle.AspNetCore
Swashbuckle.AspNetCore
→ Generates Swagger/OpenAPI docs automatically.
1.4 Application Code
Replace the content Program.cs with:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddHealthChecks(); // Adds /health endpoint
builder.Services.AddEndpointsApiExplorer(); // Enables endpoint discovery for Swagger
builder.Services.AddSwaggerGen(); // Generates Swagger UI
// Configure Kestrel to listen on port 8080 (important for containers)
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080); // Allows traffic from any network interface
});
var app = builder.Build();
// Enable Swagger UI in Dev & Prod
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather API v1");
c.RoutePrefix = "swagger"; // URL: /swagger
});
}
// Welcome endpoint
app.MapGet("/", () => new
{
Message = "Welcome to the Weather App!",
Version = "1.0.0",
Environment = app.Environment.EnvironmentName,
Timestamp = DateTime.UtcNow
})
.WithName("GetWelcome")
.WithTags("General");
// Weather forecast endpoint
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)), // Next 5 days
TemperatureC = Random.Shared.Next(-20, 55), // Random °C
TemperatureF = 0, // Placeholder
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.Select(temp => new
{
temp.Date,
temp.TemperatureC,
TemperatureF = 32 + (int)(temp.TemperatureC / 0.5556), // Formula °C → °F
temp.Summary
});
return forecast;
})
.WithName("GetWeatherForecast")
.WithTags("Weather");
// Health check endpoint (important for Kubernetes probes)
app.MapHealthChecks("/health")
.WithTags("Health");
app.Run(); // Starts the web server
Concepts Explained:
Minimal APIs → Introduced in .NET 6, let you build APIs with fewer lines.
Swagger → Auto-generates interactive API docs.
Probes → Kubernetes uses health endpoints to restart/retry containers when needed.
1.5 Test Locally
dotnet run
Test endpoints in browser or with curl
in the terminal:
http://localhost:8080/
→ Welcome message
http://localhost:8080/weather
→ Forecast data
http://localhost:8080/swagger
→ Swagger docs
http://localhost:8080/health
→ Health check
Below is the Weather App on a browser:
Stop the app with Ctrl + C
.
🐳 Step 2: Containerize Your Application with Docker
We’ll package the .NET app into a Docker container so it can run consistently anywhere.
2.1 Create Dockerfile
Create a file named Dockerfile inside WeatherApp/
directory:
touch Dockerfile
Add the content below to the Dockerfile
created:
# Multi-stage build for optimized image size
# 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
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy project file and restore dependencies
COPY ["WeatherApp/WeatherApp.csproj", "."]
RUN dotnet restore "WeatherApp.csproj"
# Copy source code and build
COPY . .
WORKDIR /src
COPY WeatherApp/ ./WeatherApp/
WORKDIR /src/WeatherApp
RUN dotnet restore "WeatherApp.csproj"
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
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WeatherApp.dll"]
Concept: Multi-stage builds → build and runtime environments are separate. This keeps the final image small and secure.
2.2 Create .dockerignore
Prevent unnecessary files from bloating your image:
touch .dockerignore
Add the content below to the Dockerfile
created:
# Build outputs
bin/
obj/
out/
# IDE files
.vs/
.vscode/
*.user
*.suo
# OS files
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Docs
README.md
*.md
# Docker
Dockerfile*
.dockerignore
# Logs
*.log
logs/
2.3 Build & Test Container
# Build image
docker build -t weather-app:local . # Ensure your Docker Desktop is running before you run this command
Below is the built WeatherApp image in Docker Desktop app:
Now let's run the container:
# Run container
docker run -d -p 8080:8080 --name weather-test weather-app:local
Test the running app using curl
in the terminal:
# Test endpoints
curl http://localhost:8080/
curl http://localhost:8080/weather
# Clean up
docker stop weather-test
docker rm weather-test
Concept: Containers make your app portable — works the same on your laptop and in the cloud.
Step 3: Set Up Azure Infrastructure
We’ll use Azure CLI to provision resources.
3.1 Login to Azure
az login #Opens browser for Azure sign-in.
3.2 Select Subscription
az account list --output table # See subscriptions
az account set --subscription "Your-Subscription-Name"
3.3 Create Resource Group
az group create --name student-demo --location eastus
Concept: A resource group is a logical container for all your Azure resources.
3.4 Create Container Registry (ACR)
az acr create --resource-group student-demo --name studentdemo2042acr --sku Basic
Concept: ACR = private Docker Hub inside Azure.
3.5 Build & Push Image
az acr build --registry studentdemo2042acr --image weather-app:latest .
Why ACR build? → Azure builds inside the cloud, avoiding OS/CPU compatibility issues.
3.6 Create AKS Cluster
az aks create \
--resource-group student-demo \
--name student-aks-cluster \
--node-count 1 \
--node-vm-size Standard_B2s \
--attach-acr studentdemo2042acr \
--enable-managed-identity \
--generate-ssh-keys
📌 Concept:
AKS = Azure-managed Kubernetes cluster
attach-acr → avoids ImagePullBackOff errors by linking AKS to ACR automatically
3.7 Connect to Cluster
az aks get-credentials \
--resource-group student-demo \
--name student-aks-cluster
kubectl get nodes # Verify cluster connection
☸️ Step 4: Deploy with Kubernetes
Kubernetes uses YAML manifests to declare your app’s state.
cd .. # Back to weather-app-demo folder
mkdir k8s
cd k8s
touch deployment.yaml
4.1 Create Deployment
Copy and paste the content below in k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-app
labels:
app: weather-app
version: v1
spec:
replicas: 2 # Run 2 pods for high availability
selector:
matchLabels:
app: weather-app
template:
metadata:
labels:
app: weather-app
version: v1
spec:
containers:
- name: weather-app
image: studentdemo2024acr.azurecr.io/weather-app:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources: # Resource requests & limits
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# Probes for self-healing
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
📌 Concepts:
Deployment → ensures the desired # of pods run.
Probes → tell Kubernetes when a pod is healthy or ready.
4.2 Create Service
Create k8s/service.yaml
file
touch service.yaml
Copy and paste the content below in k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: weather-app-service
spec:
type: LoadBalancer # Exposes app to internet
selector:
app: weather-app
ports:
- port: 80 # External port
targetPort: 8080 # Maps to container
📌 Concept: A Service gives your pods a stable IP address + load balancing.
4.3 Deploy
# Apply Kubernetes manifests
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
# Check deployment status
kubectl get deployments
kubectl get pods
# Check deployment status
kubectl get services
🔄 Step 5: GitHub Actions CI/CD
We’ll automate build → push → deploy.
5.1 Init Git
cd .. # Back to weather-app-demo folder
# Initialize Git repository
git init
git add .
git commit -m "Initial commit: Weather App"
5.2 Create Service Principal
# Get your subscription ID
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
echo "Subscription ID: $SUBSCRIPTION_ID"
# 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
Save JSON output → add as GitHub Secret AZURE_CREDENTIALS
.
5.3 Create GitHub Repo
gh auth login # Follow the prompts
# Create GitHub repository (using GitHub CLI) and Push to Repo
gh repo create weather-app-demo --public --source=. --push
Alternative: Create the repository manually on GitHub.com and push your code.
5.4 Configure GitHub Secrets
Use JSON output from step 5.2
gh secret set AZURE_CREDENTIALS -b'PASTE_JSON_HERE'
gh secret set ACR_NAME -b"studentdemo2042acr"
gh secret set RESOURCE_GROUP -b"student-demo"
gh secret set CLUSTER_NAME -b"student-aks-cluster"
5.5 Create a GitHub Workflow
Create the workflow directory and file:
mkdir -p .github/workflows
touch .github/workflows/deploy.yml
Copy and paste the content below in .github/workflows/deploy.yml
file.
name: Build and Deploy to AKS
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
IMAGE_NAME: weather-app
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
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 || true
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Setup Azure CLI
uses: azure/cli@v2
with:
inlineScript: echo "Azure CLI setup complete"
- name: Build and push Docker image to ACR
run: |
IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
az acr build \
--registry ${{ secrets.ACR_NAME }} \
--image ${{ env.IMAGE_NAME }}:${{ github.sha }} \
--file WeatherApp/Dockerfile \
.
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- name: Deploy to AKS
if: github.ref == 'refs/heads/main'
run: |
echo "Checking if AKS cluster exists..."
if ! az aks show --resource-group "${{ secrets.RESOURCE_GROUP }}" --name "${{ secrets.CLUSTER_NAME }}" --output table; then
echo "ERROR: AKS cluster '${{ secrets.CLUSTER_NAME }}' not found in resource group '${{ secrets.RESOURCE_GROUP }}'"
exit 1
fi
echo "Getting AKS credentials..."
az aks get-credentials \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--name ${{ secrets.CLUSTER_NAME }} \
--overwrite-existing
echo "Testing kubectl connection..."
kubectl cluster-info
echo "Updating deployment with image: ${{ env.IMAGE_TAG }}"
kubectl set image deployment/weather-app \
weather-app=${{ env.IMAGE_TAG }}
echo "Waiting for rollout to complete..."
kubectl rollout status deployment/weather-app --timeout=600s
echo "Deployment status:"
kubectl get pods -l app=weather-app
kubectl get service weather-app-service
📌 Concept:
GitHub Actions → automates the whole pipeline.
Every push to
main
→ build → push image → deploy to AKS.
5.6 Deploy Your Application
git add .
git commit -m "Add GitHub Actions CI/CD pipeline"
git push origin main
Monitor the deployment:
- Go to your GitHub repository
- Click the Actions tab
- Watch your workflow run in real-time
🌐 Step 6: Access Your Deployed App
6.1 Get External IP Address
# Check service status
kubectl get service weather-app-service
# Wait for EXTERNAL-IP (may take 2-5 minutes)
kubectl get service weather-app-service --watch
Wait until EXTERNAL-IP
is assigned. Then test:
# Test the endpoints
curl http://YOUR-EXTERNAL-IP/
curl http://YOUR-EXTERNAL-IP/weather
curl http://YOUR-EXTERNAL-IP/health
# Or open in browser
open http://YOUR-EXTERNAL-IP/swagger # macOS
start http://YOUR-EXTERNAL-IP/swagger # Windows
🔄 Step 7: Test Continuous Deployment
- Edit
Program.cs
welcome message.
app.MapGet("/", () => new
{
Message = "Welcome to the Updated Weather App! 🌤️",
Version = "1.1.0",
Environment = app.Environment.EnvironmentName,
Timestamp = DateTime.UtcNow,
DeployedBy = "GitHub Actions"
})
- Commit & push:
git add .
git commit -m "Update welcome message"
git push origin main
- GitHub Actions will rebuild & redeploy automatically.
- Test again → you should see your new message.
curl http://YOUR-EXTERNAL-IP/
Conclusion
You’ve just taken a local .NET 8 Web API and transformed it into a cloud-native application running on Azure Kubernetes Service, packaged with Docker, and automated with a GitHub Actions CI/CD pipeline. That’s the full lifecycle of modern cloud development — from writing code → to shipping containers → to running resilient workloads in production.
The best part? You now have a repeatable blueprint you can apply to almost any project:
Swap the Weather API with your own service
Reuse the Docker + Kubernetes setup
Extend your GitHub workflow for tests, security scans, or staging environments
This isn’t just about building an app — it’s about learning how to ship production-ready software at scale.
Keep experimenting: add monitoring, autoscaling, or connect a database. The cloud-native journey is iterative, but now you’ve got the foundations locked in.
Top comments (0)