DevOpsil
Azure
92%
Needs Review

Azure AKS: Production-Ready Cluster Setup in 15 Minutes

Dev PatelDev Patel6 min read

Azure AKS: Production-Ready Cluster Setup in 15 Minutes

Most AKS tutorials show you the bare minimum — a single az aks create command that spins up a cluster nobody would trust in production. This guide skips the toy examples and walks you through everything you actually need: private networking, autoscaling node pools, Azure AD RBAC integration, and observability wired up from day one.

Prerequisites

  • Azure CLI 2.50+ installed and logged in (az login)
  • An active subscription with Contributor or Owner access
  • A resource group and virtual network already provisioned (or follow the commands below)

Step 1: Foundation — Resource Group and Networking

# Variables — adjust to your environment
RESOURCE_GROUP="rg-aks-prod"
LOCATION="eastus2"
CLUSTER_NAME="aks-prod-01"
VNET_NAME="vnet-aks-prod"
SUBNET_NAME="snet-aks-nodes"

# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION

# Create VNet with a /16 range
az network vnet create \
  --resource-group $RESOURCE_GROUP \
  --name $VNET_NAME \
  --address-prefix 10.10.0.0/16 \
  --subnet-name $SUBNET_NAME \
  --subnet-prefix 10.10.1.0/24

# Grab the subnet ID for later
SUBNET_ID=$(az network vnet subnet show \
  --resource-group $RESOURCE_GROUP \
  --vnet-name $VNET_NAME \
  --name $SUBNET_NAME \
  --query id -o tsv)

Keeping the subnet dedicated to AKS nodes avoids IP exhaustion issues when pods scale up. Use Azure CNI (not kubenet) so pods get real VNet IPs and network policies work properly.

Step 2: Create the AKS Cluster

az aks create \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --location $LOCATION \
  --kubernetes-version 1.29 \
  --node-count 3 \
  --node-vm-size Standard_D4ds_v5 \
  --os-disk-size-gb 128 \
  --vnet-subnet-id $SUBNET_ID \
  --network-plugin azure \
  --network-policy calico \
  --enable-cluster-autoscaler \
  --min-count 2 \
  --max-count 10 \
  --enable-aad \
  --enable-azure-rbac \
  --enable-managed-identity \
  --enable-addons monitoring \
  --workspace-resource-id "/subscriptions/<sub-id>/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.OperationalInsights/workspaces/law-aks-prod" \
  --zones 1 2 3 \
  --uptime-sla \
  --generate-ssh-keys

Key flags explained:

FlagWhy It Matters
--network-policy calicoEnables pod-level firewall rules
--enable-azure-rbacMaps Azure AD groups to Kubernetes RBAC roles
--zones 1 2 3Spreads nodes across availability zones
--uptime-slaGuarantees 99.95% API server uptime (paid)
--enable-addons monitoringDeploys Container Insights agent automatically

Step 3: Add a Spot Node Pool for Batch Workloads

Your system pool handles critical services. Add a spot pool for cost-sensitive batch jobs:

az aks nodepool add \
  --resource-group $RESOURCE_GROUP \
  --cluster-name $CLUSTER_NAME \
  --name spotpool \
  --node-count 0 \
  --node-vm-size Standard_D8ds_v5 \
  --priority Spot \
  --eviction-policy Delete \
  --spot-max-price -1 \
  --enable-cluster-autoscaler \
  --min-count 0 \
  --max-count 20 \
  --node-taints "kubernetes.azure.com/scalesetpriority=spot:NoSchedule" \
  --labels spot=true

Pods that tolerate spot nodes use this cheaper pool. Everything else stays on the system pool. Scale-to-zero (--min-count 0) means you pay nothing when the pool is idle.

Step 4: Connect kubectl and Verify

az aks get-credentials \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --overwrite-existing

kubectl get nodes -o wide

Expected output:

NAME                                STATUS   ROLES    AGE   VERSION   INTERNAL-IP
aks-nodepool1-12345678-vmss000000   Ready    <none>   2m    v1.29.2   10.10.1.4
aks-nodepool1-12345678-vmss000001   Ready    <none>   2m    v1.29.2   10.10.1.5
aks-nodepool1-12345678-vmss000002   Ready    <none>   2m    v1.29.2   10.10.1.6

Step 5: RBAC — Assign Azure AD Groups to Cluster Roles

# Get the cluster resource ID
AKS_ID=$(az aks show \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --query id -o tsv)

# Grant an AD group cluster-admin access
az role assignment create \
  --role "Azure Kubernetes Service RBAC Cluster Admin" \
  --assignee-object-id "<ad-group-object-id>" \
  --scope $AKS_ID

# Grant a dev team read-only access to a namespace
az role assignment create \
  --role "Azure Kubernetes Service RBAC Reader" \
  --assignee-object-id "<dev-team-group-id>" \
  --scope "$AKS_ID/namespaces/app-production"

This eliminates kubeconfig sharing. Developers authenticate via az login and get only what their AD group allows.

Step 6: Deploy a Test Workload with Pod Disruption Budget

# test-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-test
  template:
    metadata:
      labels:
        app: nginx-test
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: nginx-test
      containers:
        - name: nginx
          image: nginx:1.25
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-test-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: nginx-test
kubectl apply -f test-deploy.yaml
kubectl get pods -o wide

The topologySpreadConstraints ensures pods land in different availability zones, and the PDB prevents node drains from taking your entire deployment offline simultaneously.

Step 7: Enable Workload Identity (Replacing Pod Identity)

# Enable OIDC issuer and workload identity
az aks update \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --enable-oidc-issuer \
  --enable-workload-identity

# Get the OIDC issuer URL
OIDC_ISSUER=$(az aks show \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --query "oidcIssuerProfile.issuerUrl" -o tsv)

echo "OIDC Issuer: $OIDC_ISSUER"

Workload identity lets pods authenticate to Azure services (Key Vault, Storage, SQL) without storing credentials in secrets. It's the modern replacement for pod-managed identity.

Production Checklist

Before you call this cluster production-ready, verify these items:

CheckCommand
API server accessiblekubectl cluster-info
All nodes Readykubectl get nodes
Container Insights data flowingAzure Portal > AKS > Insights
Calico network policies installedkubectl get pods -n kube-system | grep calico
Spot pool at zero when idlekubectl get nodes -l spot=true
PDB on all critical deploymentskubectl get pdb -A
Workload identity enabledaz aks show ... --query "oidcIssuerProfile"

Cost Estimate

For a production cluster in East US 2 with 3 x Standard_D4ds_v5 nodes plus autoscaling to 10:

ComponentMonthly Estimate
3 system nodes (always on)~$420
AKS uptime SLA~$73
Log Analytics workspace~$30
Spot nodes (20% utilization)~$40
Total~$563/month

Spot savings alone can cut 60-80% off node costs for batch workloads.

What's Next

This cluster is functional, but a real production environment also needs:

  • Ingress controller: NGINX or Application Gateway Ingress Controller (AGIC)
  • Certificate management: cert-manager with Let's Encrypt or Key Vault integration
  • GitOps: Flux v2 or Argo CD for declarative deployments
  • Image scanning: Defender for Containers or Trivy in your CI pipeline
  • Backup: Velero with Azure Blob Storage for etcd and PV snapshots

The foundation you built here — CNI networking, Azure AD RBAC, autoscaling, and availability zones — is what separates a demo cluster from one your on-call team can actually sleep through.

Share:

Was this article helpful?

Dev Patel
Dev Patel

Cloud Cost Optimization Specialist

I find the money your cloud is wasting. FinOps practitioner, data-driven analyst, and the person your CFO wishes they'd hired sooner. Every dollar saved is a dollar earned.

Related Articles

Discussion