Step-by-Step Guide: Deploying ASP.NET Core Web API with EF Core & SQL Server on AKS using Bicep & ACR
In today’s cloud-driven world, hosting ASP.NET Core APIs on Azure Kubernetes Service (AKS) using infrastructure-as-code (IaC) provides scalability, performance, and operational efficiencies. This guide walks real-world developers through containerizing and deploying a complete ASP.NET Core Web API integrated with EF Core and SQL Server. We’ll leverage Bicep for Azure resource provisioning and push container images to Azure Container Registry (ACR), as recommended by Microsoft (Microsoft, 2022). Let’s dive into the details.
Introduction: Why Host ASP.NET Core APIs on AKS with Infrastructure as Code
Deploying ASP.NET Core applications on AKS provides the robustness of Kubernetes combined with advanced features like auto-scaling, rolling updates, and managed services. With approximately 7.1% of developers using ASP.NET Core according to Stack Overflow (Stack Overflow, 2021), and AKS being recognized in Gartner’s Leaders quadrant (Gartner, 2021), this setup offers a mature ecosystem and best practices for scalable cloud hosting. Using Bicep streamlines resource deployment, ensuring environments are predictable and repeatable.
// ASCII Architecture Diagram:
//
// +--------------+
// | Clients |
// +------+-------+
// |
// +------+-------+ +---------------+
// | AKS | ---> | Azure SQL DB |
// | (API Service)| +---------------+
// +------+-------+
// |
// +------+-------+
// | Container |
// | Registry |
// +--------------+
Prerequisites: Tools, Accounts, and Knowledge You’ll Need
Before starting, ensure you have a strong understanding of C#, ASP.NET Core, and EF Core along with Docker and Kubernetes concepts. Also, prepare the following prerequisites:
- An active Azure subscription.
- Azure CLI installed (https://docs.microsoft.com/en-us/cli/azure/install-azure-cli).
- Bicep CLI installed (https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/install).
- Docker Desktop installed (https://www.docker.com/products/docker-desktop).
- IDE such as Visual Studio or Visual Studio Code with C# extensions.
- Basic understanding of Kubernetes YAML, Helm, and deployment pipelines.
// Typical folder structure for the project:
/MultiTenantApi
/Controllers
/Models
/Data
/Migrations
/Services
/Docker
/Kubernetes
/Bicep
Step 1: Create ASP.NET Core Web API with EF Core and SQL Server Integration
Start by creating a simple ASP.NET Core Web API project. This project will use EF Core for ORM connectivity to a SQL Server instance. For our example, we will build a product management API.
// Create a new ASP.NET Core Web API project:
dotnet new webapi -o MultiTenantApi
// In your MultiTenantApi/Models directory, create a Product entity:
namespace MultiTenantApi.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
// In your MultiTenantApi/Data directory, setup the DbContext:
using Microsoft.EntityFrameworkCore;
using MultiTenantApi.Models;
namespace MultiTenantApi.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options)
{
}
public DbSet Products { get; set; }
}
}
// Register the DbContext in Program.cs:
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
This API follows best practices by isolating database context and using dependency injection. Avoid “anemic entities” by encapsulating business logic within domain entities when applicable.
Step 2: Containerize Your App with Docker and Push to Azure Container Registry (ACR)
Create a Dockerfile to containerize the ASP.NET Core API. We will then build the Docker image, test it locally, and push it to Azure Container Registry (ACR), leveraging Docker best practices as highlighted by JetBrains (2022) for containerized development.
# MultiTenantApi/Docker/Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MultiTenantApi.csproj", "./"]
RUN dotnet restore "./MultiTenantApi.csproj"
COPY . .
RUN dotnet build "MultiTenantApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MultiTenantApi.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MultiTenantApi.dll"]
After building the image locally, tag and push the image to ACR:
# Login to Azure CLI and ACR
az login
az acr login --name YourACRName
# Build the Docker image
docker build -t youracrname.azurecr.io/multitenantapi:latest -f Docker/Dockerfile .
# Push the Docker image to ACR
docker push youracrname.azurecr.io/multitenantapi:latest
This versioning and tagging strategy ensures your images remain manageable and easily deployable. The before version of the Dockerfile could have combined build and runtime steps, but separating these phases improves caching and simplifies debugging.
Step 3: Use Bicep to Provision AKS, ACR, and SQL Server Resources Securely
Using Bicep to describe your infrastructure is a best practice for IaC. It allows for a simplified syntax over ARM templates. Our Bicep script will create an AKS cluster, an Azure Container Registry for our images, and an Azure SQL Server instance.
// MultiTenantApi/Bicep/main.bicep
param aksName string = 'myAKSCluster'
param acrName string = 'myacr${uniqueString(resourceGroup().id)}'
param sqlServerName string = 'sqlserver${uniqueString(resourceGroup().id)}'
param sqlAdminUsername string = 'sqladmin'
@secure()
param sqlAdminPassword string
resource acr 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
name: acrName
location: resourceGroup().location
sku: {
name: 'Standard'
}
properties: {
adminUserEnabled: true
}
}
resource aks 'Microsoft.ContainerService/managedClusters@2021-05-01' = {
name: aksName
location: resourceGroup().location
properties: {
dnsPrefix: aksName
agentPoolProfiles: [
{
name: 'agentpool'
count: 2
vmSize: 'Standard_DS2_v2'
osType: 'Linux'
}
]
linuxProfile: {
adminUsername: 'azureuser'
ssh: {
publicKeys: [
{
keyData: 'ssh-rsa AAAAB3...'
}
]
}
}
servicePrincipalProfile: {
clientId: 'your-client-id'
secret: 'your-client-secret'
}
addonProfiles: {}
enableRBAC: true
}
}
resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = {
name: sqlServerName
location: resourceGroup().location
properties: {
administratorLogin: sqlAdminUsername
administratorLoginPassword: sqlAdminPassword
}
}
resource sqlDb 'Microsoft.Sql/servers/databases@2021-02-01-preview' = {
name: '${sqlServer.name}/MultiTenantDb'
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
maxSizeBytes: '268435456000'
sampleName: 'AdventureWorksLT'
}
}
output acrLoginServer string = acr.properties.loginServer
This modular approach in Bicep adheres to best practices for secure, repeatable deployments. Make sure to secure sensitive information such as SQL passwords by utilizing Azure Key Vault integrations later.
Step 4: Deploy ASP.NET Core Web API to AKS with Kubernetes Manifests
Now that the infrastructure is ready and our container image is stored in ACR, deploy the ASP.NET Core API on AKS. We create Kubernetes manifests for the deployment and service definitions:
# MultiTenantApi/Kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: multitenantapi-deployment
spec:
replicas: 2
selector:
matchLabels:
app: multitenantapi
template:
metadata:
labels:
app: multitenantapi
spec:
containers:
- name: multitenantapi
image: youracrname.azurecr.io/multitenantapi:latest
ports:
- containerPort: 80
env:
- name: ConnectionStrings__DefaultConnection
value: "Server=tcp:your-sql-server.database.windows.net,1433;Initial Catalog=MultiTenantDb;User ID=sqladmin;Password=yourPassword;Encrypt=true;"
---
# MultiTenantApi/Kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
name: multitenantapi-service
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: multitenantapi
This deployment YAML ensures the container is replicated and accessible through an Azure load balancer. Be cautious to never store credentials directly in Kubernetes manifests for production and prefer Kubernetes secrets.
Step 5: Configure EF Core Migration and Database Connectivity to SQL Server in AKS
Running EF Core migrations in a containerized environment requires a reliable mechanism to trigger database migrations on startup. Consider integrating migration execution in the entry point or using an init container. Below, we show a simple pattern where EF Core migrations are invoked during API startup:
// Before Refactor: Without automatic migration in Program.cs
// Manually run 'dotnet ef database update' from your development environment.
// After Refactor: Invoke migration in Program.cs
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
dbContext.Database.Migrate();
}
app.Run();
This pattern ensures your application can automatically evolve the underlying database schema. The connection string environment variable is injected via Kubernetes manifest as shown in Step 4.
Step 6: Automate with Azure DevOps or GitHub Actions (Optional CI/CD Pipeline)
Setting up a CI/CD pipeline automates testing, building, and deployment. Below is an example using GitHub Actions to build the Docker image and deploy to ACR and AKS:
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- name: Build with dotnet
run: dotnet build --configuration Release
- name: Build Docker Image
run: |
docker build -t youracrname.azurecr.io/multitenantapi:latest -f Docker/Dockerfile .
- name: Login to ACR
run: az acr login --name youracrname
env:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
- name: Push Docker Image
run: docker push youracrname.azurecr.io/multitenantapi:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to AKS
run: |
kubectl apply -f Kubernetes/deployment.yaml
kubectl apply -f Kubernetes/service.yaml
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
This pipeline embodies best practices for CI/CD by automating testing and ensuring code quality, while integrating tightly with Azure services.
Step 7: Testing, Scaling, and Monitoring Your Service on AKS
Once your application is deployed, it’s important to test for functionality, monitor performance, and scale the application as needed. Start by testing the endpoint via a tool like curl or Postman:
// Example curl test command
curl http:///api/products
For scaling, you can adjust the replica count in your Kubernetes deployment manifest and utilize Horizontal Pod Autoscaler (HPA):
# Example Kubernetes HPA configuration:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: multitenantapi-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: multitenantapi-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
For monitoring and logging, consider integrating Azure Monitor and Prometheus/Grafana to capture detailed metrics from your clusters and containers. Ensure alerts are configured for key performance indicators.
Common Mistakes When Using ASP.NET Core on AKS with IaC
- Storing sensitive data such as connection strings directly in code or Kubernetes manifests instead of using secure tools like Azure Key Vault.
- Not separating build and runtime environments in the Dockerfile, which can lead to larger images and slower build times.
- Neglecting to use specific versions for dependencies in your container images, causing unexpected behavior during runtime upgrades.
- Over-deploying a monolithic API without splitting logical components, leading to “god services” that become challenging to maintain and scale.
Who Should Use ASP.NET Core on AKS with IaC (and Who Should Avoid It)
This approach is ideal for medium to large teams looking to build scalable, cloud-native applications with strong DevOps practices. Developers comfortable with .NET, containerization, and Kubernetes will benefit from the modular architecture and repeatable IaC protocols. However, if you are managing a very simple application where orchestration overhead with Kubernetes is unnecessary, or if your team lacks experience in container orchestration, you might consider a lighter-weight cloud service or managed PaaS alternative.
Conclusion and Next Steps: Hardening and Building on this Foundation
In this tutorial, we went through creating an ASP.NET Core Web API integrated with EF Core and SQL Server, containerizing it with Docker, and deploying the solution on AKS using Bicep for IaC. We covered essential steps such as registering to ACR, deploying Kubernetes manifests, automating with CI/CD pipelines, and strategies for testing, scaling, and monitoring the service.
Moving forward, consider enhancing security by integrating Azure Key Vault, implementing role-based access control (RBAC) in AKS, and adopting advanced deployment strategies, such as blue-green deployments. By further refining your setup, you optimize for performance and resilience in production environments.