1. A CI Pipeline for Images
The first half of the pipeline: every push to main automatically
builds, tests, and pushes a new image, tagged with the exact commit SHA — the
tagging discipline from Week 6, now automated.
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: docker build --target test -t my-app:test . # a "test" stage in the Dockerfile
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
run: |
docker build -t ghcr.io/my-org/my-app:${{ github.sha }} .
docker push ghcr.io/my-org/my-app:${{ github.sha }}
Notice the image is tagged with github.sha — the exact Git commit —
never latest. This is what makes the GitOps step in Section 3 actually
work: a specific commit maps to a specific, traceable image, with no ambiguity.
Add a Trivy or Docker Scout scan step right after the build, configured to fail the pipeline on new critical/high-severity findings — exactly the "scan in CI, not just locally" idea flagged back in Week 6, now enforced automatically on every commit.
2. What GitOps Actually Means
CI (Section 1) handles building and testing. GitOps is about
deployment: instead of a pipeline running kubectl apply
directly against your cluster, a controller running inside the cluster
continuously watches a Git repo and reconciles the cluster to match it.
TRADITIONAL CI/CD PUSH MODEL:
CI pipeline --(kubectl apply, needs cluster credentials)--> Cluster
Cluster state = whatever the last pipeline run pushed. Manual kubectl
changes made directly against the cluster are invisible to Git.
GITOPS PULL MODEL:
Git repo (desired state) <--(continuously watched)-- ArgoCD (in-cluster)
ArgoCD reconciles the CLUSTER to match GIT, continuously — not just at
deploy time. A manual kubectl change gets silently reverted, because
Git, not the cluster, is the source of truth.
That last line is the whole philosophy in one sentence: Git is the single source of truth, not the cluster. If it's not in Git, it doesn't count as a legitimate change — and if the cluster drifts from Git for any reason, it gets pulled back automatically.
In the pull model, no external CI system needs cluster-admin credentials at all — ArgoCD runs inside the cluster and pulls from Git, rather than an external pipeline needing broad, standing access to push into it. This meaningfully shrinks the attack surface around your cluster's credentials.
3. ArgoCD, Set Up
Installing ArgoCD and pointing it at a Git repo containing your Helm chart from Week 13:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl port-forward svc/argocd-server -n argocd 8080:443
# open https://localhost:8080 — get the initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-app-manifests.git
targetRevision: main
path: charts/my-app
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true # remove resources deleted from Git
selfHeal: true # revert manual cluster changes back to match Git
selfHeal: true is the setting that makes GitOps's promise real —
without it, ArgoCD only syncs on demand; with it, drift is corrected automatically
and continuously, which is what Section 4 demonstrates directly.
Even ArgoCD's own configuration follows the same pattern as everything else in this course — a YAML object describing desired state (what repo, what path, what destination) that a controller reconciles toward. The pattern really is that consistent throughout Kubernetes.
4. Drift Detection, Live
Prove the whole loop works, end to end — from a Git commit to a live cluster change, and separately, from a manual cluster change back to Git's version:
# Edit values-prod.yaml in your manifests repo, bump replicaCount, commit, push
git commit -am "scale prod to 5 replicas"
git push
# Within ArgoCD's sync interval (default ~3 minutes, or instantly with a webhook):
kubectl get application my-app -n argocd # STATUS moves to OutOfSync, then Syncing, then Synced
kubectl get deployment my-app -n prod # replica count updates automatically
# Make a manual change directly against the cluster, bypassing Git entirely:
kubectl scale deployment/my-app --replicas=1 -n prod
# With selfHeal enabled, ArgoCD notices the mismatch and reverts it automatically:
kubectl get deployment my-app -n prod -w
# replicas briefly shows 1... then reverts back to 5, matching Git
That reversal is the entire point made concrete: the cluster is not the source of truth, Git is — and any change that didn't go through Git doesn't survive.
Week 18 expects a full CI/CD + GitOps pipeline as part of the final deliverable — this week's exercise builds the exact skeleton you'll extend there: a CI workflow, a manifests repo, and ArgoCD tying them together.
5. Hands-on Exercise
Wire a real CI pipeline to a real ArgoCD deployment
Build both halves end to end: a CI workflow that pushes images on commit, and an ArgoCD Application that keeps your cluster continuously matched to a Git repo.
Part 1 — CI pipeline:
- Add a GitHub Actions workflow to your app's repo that builds and pushes an image to GHCR (or your registry of choice), tagged with the commit SHA, on every push to main.
- Push a commit and confirm in the Actions tab that it runs successfully and a new image appears in your registry with the expected tag.
- Add a scan step (Trivy is free and simple to add as a GitHub Action) after the build, and confirm it runs and reports findings without necessarily failing the build yet.
The built-in GITHUB_TOKEN secret is usually enough to push to GHCR without any extra credential setup — check your repository's Package permissions if you get an authorization error.
Part 2 — ArgoCD deployment:
- Create a separate Git repo (or a folder in your existing one) holding your Week 13 Helm chart and a values-prod.yaml.
- Install ArgoCD on your local cluster and create an Application pointing at that repo/path, with
selfHeal: true. - Confirm it syncs and your app deploys via ArgoCD rather than a manual
helm install. - Perform both drift tests from Section 4: commit a values change and watch it deploy automatically, then manually
kubectl scalesomething and watch ArgoCD revert it. Write two sentences describing what you observed in each case.
If ArgoCD's default sync interval feels slow to watch during testing, click "Refresh" (or "Sync") manually in the ArgoCD UI or via argocd app sync my-app to trigger reconciliation immediately, rather than waiting for the polling interval.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
In the GitOps model, what is the single source of truth for what should be running in the cluster?
In the GitOps model, what is the single source of truth for what should be running in the cluster?
Git. The cluster's actual state is continuously reconciled to match what's declared in a Git repository — any change made directly against the cluster that isn't reflected in Git is considered drift and, with self-healing enabled, gets automatically reverted.
Q2
What's the key structural difference between a traditional CI/CD "push" pipeline and a GitOps "pull" model?
What's the key structural difference between a traditional CI/CD "push" pipeline and a GitOps "pull" model?
In a push model, an external CI system runs kubectl apply against the cluster directly, requiring it to hold cluster credentials. In a pull model, a controller (ArgoCD) runs INSIDE the cluster and pulls changes from Git — no external system needs standing cluster-admin access, reducing the attack surface.
Q3
What does ArgoCD's selfHeal: true setting actually do?
What does ArgoCD's selfHeal: true setting actually do?
It makes ArgoCD automatically revert any manual, out-of-band change made directly against the cluster (e.g. a manual kubectl scale) back to whatever is declared in Git, without requiring a human to notice the drift and fix it manually.
Q4
Why does the CI pipeline tag images with the Git commit SHA instead of a version number or "latest"?
Why does the CI pipeline tag images with the Git commit SHA instead of a version number or "latest"?
It guarantees an unambiguous, traceable link from a deployed image back to the exact commit that produced it, with no possibility of the tag being reused or reassigned — the same tagging discipline from Week 6, now essential for GitOps since a Git commit needs to map deterministically to a specific image.