The image is in ACR. AKS is running. Now we connect them — a pipeline that deploys your container to Kubernetes with zero-downtime rolling updates. Since you already know K8s, we'll focus on the CI/CD integration.

The Full Picture

CI test Build docker → ACR Scan trivy Deploy Staging helm → AKS Verify smoke test Deploy Prod approval → helm Same image through every stage — build once, deploy everywhere Push to main → Production in ~5 minutes (excluding approval)

🏋️ Step 1: Provision AKS (If Not Done)

CLUSTER_NAME="aks-cicd-mastery"
RESOURCE_GROUP="rg-cicd-mastery"

# Create AKS cluster
az aks create \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --node-count 2 \
  --node-vm-size Standard_B2s \
  --enable-managed-identity \
  --attach-acr $ACR_NAME \
  --generate-ssh-keys

# Get credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME

# Verify
kubectl get nodes

# Grant GitHub Actions access
AKS_ID=$(az aks show -g $RESOURCE_GROUP -n $CLUSTER_NAME --query id -o tsv)
az role assignment create --assignee $APP_ID \
  --role "Azure Kubernetes Service Cluster User Role" --scope $AKS_ID
az role assignment create --assignee $APP_ID \
  --role "Azure Kubernetes Service RBAC Writer" --scope $AKS_ID

🏋️ Step 2: K8s Manifests

Create k8s/deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cicd-mastery-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cicd-mastery-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0    # Zero downtime
  template:
    metadata:
      labels:
        app: cicd-mastery-api
    spec:
      containers:
        - name: api
          image: IMAGE_PLACEHOLDER   # Replaced by pipeline
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits:   { cpu: 500m, memory: 256Mi }
---
apiVersion: v1
kind: Service
metadata:
  name: cicd-mastery-api
spec:
  type: ClusterIP
  selector:
    app: cicd-mastery-api
  ports:
    - port: 80
      targetPort: 3000

🏋️ Step 3: The AKS Deploy Pipeline

# .github/workflows/deploy-aks.yml
name: Deploy to AKS

on:
  workflow_run:
    workflows: ["Container Build"]
    types: [completed]
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    name: 🚀 Deploy to AKS
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Get AKS credentials
        run: |
          az aks get-credentials \
            --resource-group ${{ vars.RESOURCE_GROUP }} \
            --name ${{ vars.CLUSTER_NAME }} \
            --overwrite-existing

      - name: Set image and deploy
        run: |
          IMAGE="${{ vars.ACR_LOGIN_SERVER }}/cicd-mastery-api:sha-$(echo ${{ github.event.workflow_run.head_sha }} | cut -c1-7)"
          
          # Replace placeholder with actual image
          sed -i "s|IMAGE_PLACEHOLDER|$IMAGE|" k8s/deployment.yml
          
          # Apply
          kubectl create namespace staging --dry-run=client -o yaml | kubectl apply -f -
          kubectl apply -f k8s/deployment.yml -n staging
          
          # Wait for rollout to complete
          kubectl rollout status deployment/cicd-mastery-api -n staging --timeout=180s

      - name: Verify
        run: |
          kubectl get pods -n staging -l app=cicd-mastery-api
          echo "## 🚀 AKS Deployment Complete" >> $GITHUB_STEP_SUMMARY
          kubectl get pods -n staging -l app=cicd-mastery-api -o wide >> $GITHUB_STEP_SUMMARY

Critical K8s Deployment Concepts for CI/CD

kubectl rollout status — what it watches Pod (v1) ✓ Pod (v1) ✓ Pod (v2) 🔄 Waiting for deployment rollout... 1/2 replicas updated, 1 available Pod (v2) ✓ Pod (v2) ✓ deployment "cicd-mastery-api" successfully rolled out 2/2 replicas updated, 2 available ✓
kubectl rollout status blocks until ALL pods are updated and passing readiness checks — your pipeline waits for success.
rollout status is your deployment verification. It won't return success until:
  • All old pods are replaced
  • All new pods pass their readinessProbe
  • Or it times out (= deployment failed)
This is why readinessProbe on your Deployment is critical — without it, K8s can't tell the pipeline "yes, this is healthy."

🧠 Recall Check

  1. Why do we use sed to replace IMAGE_PLACEHOLDER instead of hardcoding the image in the manifest?
  2. What does maxUnavailable: 0 guarantee during a rolling update?
  3. Why is kubectl rollout status --timeout=180s essential in a pipeline?
  4. What triggers this workflow? (Hint: look at the on: block)
Reveal answers
  1. The image tag changes every build (git SHA). We keep the manifest generic (placeholder) and inject the specific tag at deploy time. This supports "same manifest, different image per deploy."
  2. Zero downtime. K8s will never terminate an old pod until a new one is ready. At least 2 pods are always serving traffic.
  3. Without it, kubectl apply returns immediately (just submits the change). rollout status blocks until pods are ACTUALLY running — so the pipeline knows if deployment succeeded or failed.
  4. workflow_run — it triggers when the "Container Build" workflow completes successfully on main. This chains workflows: build finishes → deploy starts.
You now have a complete CI/CD pipeline deploying containers to Kubernetes. But this is push-based CD — the pipeline pushes to the cluster. In the GitOps section (Lessons 20+), we'll replace this with pull-based CD using ArgoCD, which is more secure and self-healing.

Next lesson: Helm-based Deployments — templating your manifests for multi-environment reuse.