Sample project to learn how to deploy a .NET 8 + SQLite API to Azure Kubernetes Service (AKS) using Docker, Terraform (with Azure remote backend), Azure Container Registry (ACR), and CI/CD pipelines with GitHub Actions. Includes automated workflows to create and destroy the infrastructure.
---- 📁 Project Structure
- 🔐 Requirements
- 🚀 GitHub Actions Workflows
- 🌐 Automated Deployment Flow
- ☁️ Terraform Remote Backend
▶️ Run API Locally- 🐳 Build Local Docker Image
- ☸️ Useful Kubernetes Commands
⚠️ Important Notes- 🧹 Cleanup
k8s-dotnet-aks/
├── src/
│ ├── K8sDotnetApi/ # .NET 8 Minimal API + EF Core + SQLite CRUD
│ └── k8s/ # Kubernetes manifests (PVC, Deployment, Service)
├── infra/ # Terraform: RG, ACR, AKS
├── .github/workflows/ # Workflows: terraform-create, terraform-destroy, ci, cd
└── K8sDotnetApi.sln # Visual Studio solution
- Azure subscription with a Service Principal
- GitHub repo with the following Secrets:
Secret | Description |
---|---|
AZURE_CREDENTIALS |
JSON credentials created with az ad sp create-for-rbac --sdk-auth |
VM_SSH_PUB_KEY |
Public SSH key for AKS node authentication |
VM_SSH_KEY |
(Optional) Private SSH key for manual access to AKS nodes |
File | Purpose | Trigger |
---|---|---|
terraform-create.yml |
Provisions infrastructure (AKS, ACR, etc.) | On push to main or manual dispatch |
terraform-destroy.yml |
Destroys all infrastructure | Manual dispatch from GitHub UI |
ci.yml |
Builds and tests the .NET API | On push or pull_request to main |
cd.yml |
Builds Docker image and deploys to AKS | On push to main |
- Push to
main
→ triggers Terraform Create Infra. - When finished, CI Build & Push runs:
- Fetches ACR login server from Terraform output.
- Builds Docker image of the API.
- Pushes image to ACR.
- Then, CD Deploy to AKS runs:
- Gets AKS credentials from Terraform output.
- Applies PVC, Deployment (with ACR image), and Service manifests.
- Exposes the app via a public LoadBalancer.
- To clean up, manually run Terraform Destroy Infra to avoid cost.
Terraform stores state remotely in an Azure Storage Account. The following resources are created:
- Resource Group:
tfstate-rg
- Storage Account:
tfstatek8sstore
- Container:
tfstate
You can also create them manually:
az group create -n tfstate-rg -l eastus
az storage account create -n tfstatek8sstore -g tfstate-rg -l eastus --sku Standard_LRS
az storage container create -n tfstate --account-name tfstatek8sstore
cd src/K8sDotnetApi
dotnet restore
dotnet run
# Access via: http://localhost:5164/
curl http://localhost:5164/movies
curl -X POST http://localhost:5164/movies -H "Content-Type: application/json" -d '{"title":"Matrix","year":1999}'
cd src/K8sDotnetApi
docker build -t holamundo-dotnet:local .
docker run -p 8080:8080 holamundo-dotnet:local
# Get AKS credentials locally
az aks get-credentials --name <aks-name> --resource-group <rg-name>
# View Pods
kubectl get pods
# View Services (check External IP)
kubectl get svc
# Apply manifests
kubectl apply -f src/k8s/deployment.yaml
kubectl apply -f src/k8s/service.yaml
# Get logs
kubectl logs <pod-name>
# Test container port is open (inside pod)
kubectl exec -it <pod-name> -- curl localhost:8080/movies
# Delete everything
kubectl delete -f src/k8s/
- The ACR name in Terraform is
k8sdotnetacr
(must be globally unique). - The
deployment.yaml
usescontainerPort: 8080
, so service must maptargetPort: 8080
. cd.yml
replaces the image reference dynamically with the one from ACR.- SQLite is persisted in
/app/data/movies.db
using a PVC backed by Azure Disk. - For production or horizontal scaling, switch to an external database like Azure SQL.
To avoid unwanted Azure costs, go to GitHub → Actions tab → manually trigger:
Terraform Destroy Infra
Enjoy learning AKS and GitHub Actions! 🚀
Once deployed, you can test the API with some sample movie records:
curl -X POST http://<EXTERNAL-IP>/movies -H "Content-Type: application/json" -d '{"title":"Matrix","year":1999}'
curl -X POST http://<EXTERNAL-IP>/movies -H "Content-Type: application/json" -d '{"title":"Inception","year":2010}'
curl -X POST http://<EXTERNAL-IP>/movies -H "Content-Type: application/json" -d '{"title":"Interstellar","year":2014}'
curl -X POST http://<EXTERNAL-IP>/movies -H "Content-Type: application/json" -d '{"title":"The Dark Knight","year":2008}'
curl -X POST http://<EXTERNAL-IP>/movies -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded","year":2003}'
Replace <EXTERNAL-IP>
with the public IP of your hola-mundo-dotnet-svc
LoadBalancer.