Skip to content
CAMPUX Cloud Bootcamp Advanced Build · GitOps ← All labs
Advanced Build · Advanced
~4–5 hrs · Costs real money (AKS)
CLI · kubectl · Helm · ArgoCD · torn down
Delivery · GitOps

Deploy by merge, not by hand.

In this build the cluster stops taking orders from your laptop and starts taking them from Git. You declare what each environment should look like in a repository; ArgoCD watches that repo and makes the cluster match it — continuously. Promote to production by merging a one-line change, roll back by reverting it, and wire a GitHub Actions pipeline that closes the loop from commit to running pod without anyone touching kubectl apply.

Fig. 1 · GitOps: the cluster matches the repo
Resource group · campux-gitops-rgcontinuous sync · self-healGit repoHelm charts · 3 envsAKS + ArgoCDthe sync agent in-clusterthe cluster matches the repo, not your memory
Start

Run one step, then check it

Read the step before copying. Bash commands belong in Git Bash on Windows or Azure Cloud Shell set to Bash; PowerShell uses different syntax. A JSON, YAML, Bicep or application-code example belongs in the named file, not directly at the terminal prompt. Follow any shell-specific instructions below.

Keep the same terminal open so earlier variables remain available. Copy one complete command block without the terminal's $ prompt. A backslash continues a command onto the next line. If Bash shows a lone >, it is waiting for the rest of a quote, command or file block. Finish the complete block; if the paste was incomplete, press Ctrl+C and copy it again. Do not paste the next step into an unfinished command.

A line such as RG="training-group" stores a value without printing it. Output captured with $(...) is also stored; a blank screen alone does not prove failure. A sleep deliberately waits, and -o none suppresses normal output. Wait for your normal prompt, then run the step's checkpoint. After an error, stop and keep the exact command and message; later steps cannot repair a missing resource. Never share passwords, access tokens or secret values in a screenshot.

Why

The cluster should match the repo, not your memory

GitOps puts the desired manifests in Git and configures a controller — here Argo CD — to compare them with the Kubernetes resources it manages. Automated sync applies Git changes; self-heal corrects direct edits to managed resources when enabled. Neither setting automatically repairs an unhealthy application or undoes a database migration.

You will install Argo CD on AKS, create one Helm chart with values for dev → QA → prod, watch a deliberately broken image become unhealthy, revert that change in Git, then have GitHub Actions build an image and commit its tag for the dev environment.

If it is not in Git, it is not in production.

This one costs money — read before you start

AKS nodes, ACR storage, and related resources can incur charges. Pricing depends on your region, subscription, and time spent; check your Azure estimate and set a budget alert first. Use a dedicated resource group so teardown is clear.

Before you begin — prerequisites

You need an Azure subscription that can create AKS and ACR, Azure CLI, kubectl, Helm and a disposable public GitHub repository. Public read access lets Argo CD fetch the training chart; keep it free of secrets. Follow Class 23’s OIDC setup before the optional image workflow. That Azure identity needs reviewed permission to build in the training ACR; the cluster needs permission to pull the image. These are separate from GitHub write permission.

Setup

A cluster and a repository

Create a small AKS cluster and a Git repository — the two ends of the GitOps loop. The repo is where you will declare desired state; the cluster is where ArgoCD makes it real.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.

# Windows/Git Bash: leave resource-id args alone (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

RG="campux-gitops-rg"
az group create -n "$RG" -l eastus
# if Azure says this size "is not allowed in your subscription", the same error lists the sizes
# that are; pick a 2-vCPU one from that list (Standard_D2as_v4 worked for us in October 2026)
az aks create -g "$RG" -n campux-gitops-aks \
  --tier free --node-count 2 --node-vm-size Standard_D2as_v4 \
  --enable-managed-identity --generate-ssh-keys
az aks get-credentials -g "$RG" -n campux-gitops-aks

# a registry for Step 5's pipeline to push to — name must be globally unique
ACR="campuxgitops$RANDOM"
az acr create -g "$RG" -n "$ACR" --sku Basic
az aks update -g "$RG" -n campux-gitops-aks --attach-acr "$ACR"
echo "$ACR.azurecr.io"   # the registry host to push to in Step 5

# Create an empty public GitHub repo in your project first.
# Copy its HTTPS clone URL and replace the example below.
REPO_URL="https://github.com/YOUR_OWNER/campux-gitops.git"
git clone "$REPO_URL" campux-gitops && cd campux-gitops
ACR_LOGIN="$(az acr show -n "$ACR" --query loginServer -o tsv)"
echo "$ACR_LOGIN"
Checkpoint kubectl get nodes shows two Ready nodes. ACR_LOGIN ends in .azurecr.io. The repo is publicly readable but contains no credentials. Stop if the repo URL is still the example.
Step 1

Install ArgoCD, the agent in the cluster

ArgoCD installs as a set of controllers in its own namespace. It watches your Git repo and drives the cluster toward what it finds there. Expose the UI just long enough to log in and get the initial password.

kubectl create namespace argocd
# --server-side avoids "metadata.annotations: Too long" on this manifest's large CRDs
kubectl apply -n argocd --server-side \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl rollout status deploy/argocd-server -n argocd

# reach the UI locally (no public LoadBalancer needed for a lab)
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
# initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d; echo
Checkpoint Browse to https://localhost:8080 and log in as admin with that password. Your browser may warn about the lab's self-signed certificate. The UI has no Applications yet. Change the initial password if the lab will remain running.
Step 2

One Helm chart, three environments

Use helm create to generate a complete Deployment, Service, probes, and template helpers. Set a known nginx image and one values file per namespace. The image is deliberately simple so the GitOps behavior is the lesson; these are lab environments, not three independently validated releases.

# Run inside the cloned campux-gitops repo.
helm create app
cat > Dockerfile <<'EOF'
FROM nginx:1.27.5-alpine
EOF
for env in dev qa prod; do
  replicas=1
  [ "$env" = qa ] && replicas=2
  [ "$env" = prod ] && replicas=3
  cat > "app/values-$env.yaml" <<EOF
replicaCount: $replicas
image:
  repository: nginx
  tag: "1.27.5-alpine"
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
EOF
  helm lint app -f "app/values-$env.yaml"
  helm template "campux-$env" app -f "app/values-$env.yaml" > /dev/null
done
git add app Dockerfile && git commit -m "Create the GitOps lab chart"
git branch -M main
git push -u origin main
Checkpoint All three lint and render commands finish without errors. Your repo contains the generated chart and values files. Keep REPO_URL from Setup: the Applications will pull this commit.
Step 3

Declare an Application per environment

An Argo CD Application identifies a repo path, a values file, and a destination namespace. Here you bootstrap these Application objects directly; the chart and values they point to are in Git.

for env in dev qa prod; do
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: campux-$env
  namespace: argocd
spec:
  project: default
  source:
    repoURL: $REPO_URL
    path: app
    targetRevision: main
    helm:
      valueFiles: [values-$env.yaml]
  destination:
    server: https://kubernetes.default.svc
    namespace: $env
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [CreateNamespace=true]
EOF
done
kubectl get applications -n argocd

The loop creates all three Applications with separate namespaces. These Application objects are bootstrapped with kubectl for this lab; they are not themselves in Git yet. A production setup can manage them through an app-of-apps or ApplicationSet pattern.

Checkpoint All three Applications appear in Argo CD and eventually show Synced / Healthy. If one fails, inspect its Events and confirm the repo is publicly readable. A Git push is applied on a later sync, not necessarily within one minute.
Step 4

Observe a failed image, then revert in Git

selfHeal: true corrects live drift from Git; it does not detect an unhealthy image and restore the last good Git commit. The generated chart includes workload probes. Break only the dev Application in this lab and watch its sync and health separately. Do not try this on a real production service.

# Change only the dev tag to a nonexistent image tag.
sed -i 's/tag: "1.27.5-alpine"/tag: "campux-intentionally-missing"/' app/values-dev.yaml
git add app/values-dev.yaml
git commit -m "Lab: test a missing dev image" && git push

# Give Argo CD time to sync, then inspect the rollout.
kubectl get application campux-dev -n argocd -o wide
kubectl get pods -n dev
kubectl describe deployment campux-dev-app -n dev

The Application may show Synced while its workload is Progressing or later Degraded. The exact phase depends on rollout timing. The previous ReplicaSet may keep some pods serving, but available capacity is not guaranteed; inspect it instead of assuming continuity. Recover by reverting the bad Git commit.

git revert --no-edit HEAD && git push
kubectl get application campux-dev -n argocd -o wide
# Wait for Synced / Healthy in Argo CD, then check the live Deployment.
kubectl rollout status deployment/campux-dev-app -n dev --timeout=5m
kubectl get pods -n dev
kubectl get deployment campux-dev-app -n dev -o jsonpath='{.spec.template.spec.containers[0].image}'; echo
Checkpoint After the revert syncs, the dev Deployment becomes ready again and the Application returns to Synced / Healthy. If it does not, inspect pod Events and the Application condition. This is a manual Git revert followed by verification, not Argo CD auto-rollback.
Step 5

Close the loop with GitHub Actions

The optional final step builds the repository’s Dockerfile in ACR, then records its dev image in Git. Create a GitHub environment named training-gitops and configure the federated credential to match this repository and environment. Set its non-secret variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, ACR_NAME and ACR_LOGIN_SERVER. Use Class 23’s identity setup and verify its ACR build permission. The training repository must allow its GITHUB_TOKEN to write contents; do not weaken a protected production branch. This manual workflow writes directly to the disposable main branch. Production promotion needs a reviewed change.

name: Training GitOps image
on:
  workflow_dispatch:
permissions:
  contents: write
  id-token: write
jobs:
  image:
    runs-on: ubuntu-latest
    environment: training-gitops
    steps:
      - uses: actions/checkout@v6
      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
      - name: Build and record the dev image
        env:
          ACR_NAME: ${{ vars.ACR_NAME }}
          ACR_LOGIN_SERVER: ${{ vars.ACR_LOGIN_SERVER }}
        run: |
          set -euo pipefail
          test -n "$ACR_NAME"
          test -n "$ACR_LOGIN_SERVER"
          TAG="$GITHUB_SHA"
          az acr build --registry "$ACR_NAME" --image "campux/app:$TAG" .
          sed -i -E "s|^  repository:.*$|  repository: $ACR_LOGIN_SERVER/campux/app|" app/values-dev.yaml
          sed -i -E "s|^  tag:.*$|  tag: '$TAG'|" app/values-dev.yaml
          git config user.email 'training-workflow@example.invalid'
          git config user.name 'Campux training workflow'
          git add app/values-dev.yaml
          if git diff --cached --quiet; then exit 0; fi
          git commit -m "ci: dev image $TAG"
          git push origin HEAD:main

Save the supplied file as .github/workflows/gitops-image.yml, commit it on main, and start Training GitOps image from Actions. It runs manually only. Observe the ACR task, the values-file commit and Argo CD’s dev reconciliation. If branch protection refuses the push, preserve it and use a reviewed pull request.

Checkpoint Confirm the ACR build pushed the commit-tagged image, the values file records it, and the dev Application reaches Synced / Healthy with that image. This workflow changes Git; Argo CD reconciles the cluster.
Down

Tear it down

Remove only the dedicated group you created for this lab. Check its name and contents first.

az group delete -n campux-gitops-rg --yes --no-wait
az group exists -n campux-gitops-rg      # -> false once the async delete finishes
Checkpoint az group exists returns false after deletion completes. The GitHub repository, workflow, environment and federated trust remain. Retire the disposable workflow and identity permissions separately.
End

What you can now honestly claim

You configured Argo CD to reconcile a Helm chart across three AKS lab namespaces, observed a failed dev image, reverted it through Git, and configured a GitHub Actions pipeline to publish an image and commit its dev tag. You can describe the sync and self-heal settings separately from health status. This lab uses direct pushes to its lab branch; production promotion needs review, image provenance, monitoring, and a rollback plan for data.

Footnotes
  1. Argo CD polls repositories on a configurable interval; a webhook can request a refresh sooner, but it does not guarantee an immediate healthy rollout.
  2. The three Application objects in this lab were bootstrapped with kubectl. Storing those declarations in Git through an app-of-apps or ApplicationSet design is a separate step.
  3. Traffic-weighted canary delivery needs additional routing and analysis, for example Argo Rollouts with a supported traffic provider. Self-heal and health checks alone do not provide it or automatic rollback.