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
🏋️ 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 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)
🧠 Recall Check
- Why do we use
sedto replace IMAGE_PLACEHOLDER instead of hardcoding the image in the manifest? - What does
maxUnavailable: 0guarantee during a rolling update? - Why is
kubectl rollout status --timeout=180sessential in a pipeline? - What triggers this workflow? (Hint: look at the
on:block)
Reveal answers
- 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."
- Zero downtime. K8s will never terminate an old pod until a new one is ready. At least 2 pods are always serving traffic.
- Without it,
kubectl applyreturns immediately (just submits the change).rollout statusblocks until pods are ACTUALLY running — so the pipeline knows if deployment succeeded or failed. 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.