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.
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.
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.
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.
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"
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.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
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.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
REPO_URL from Setup: the Applications will pull this commit.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.
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
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.
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
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.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.
- Argo CD polls repositories on a configurable interval; a webhook can request a refresh sooner, but it does not guarantee an immediate healthy rollout.
- 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. - 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.