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 Dockerfilecreated:
# 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 Dockerfilecreated:
# 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.cswelcome 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)