Hands-on: upgrade tasks, a reference image-build workflow and a Kluctl GitOps example in sharifndlovu123/devops-directive-kubernetes-course. Pipeline concepts: 1. CI-CD Pipelines; workflow syntax: GitHub Actions Deep Dive.
Two operational jobs keep a cluster healthy: delivering changes to it and upgrading it.
1. CI/CD for Kubernetes
The Kubernetes-specific part of a pipeline has four steps:
flowchart LR
C[Commit] --> B[1. Build image]
B --> P[2. Push to registry]
P --> M[3. Update manifests with the new image tag]
M --> A[4. Apply to the cluster]
CI still also lints, tests and scans, but those are not Kubernetes-specific (5. Secure Coding and Supply Chain).
Practices:
- Tag images immutably (commit SHA or semantic version), never latest
- Build once, promote the same image from staging to production
- Scan the image and sign it; verify signatures at deploy time
- Updating tags by string replacement in a script is brittle; use a proper mechanism (Flux image automation, Renovate, Kustomize images, Helm values)
# build job (fragment)
- uses: docker/build-push-action@<sha>
with:
push: true
tags: ghcr.io/${{ github.repository }}/api:${{ github.sha }}
2. Keeping the Cluster in Sync
| Method | How | Problem |
|---|---|---|
Manual kubectl apply |
A person runs it | Someone forgets, or targets the wrong cluster |
| Pipeline push | CI runs kubectl apply or helm upgrade |
Cluster credentials live in the CI system; no drift detection |
| GitOps (pull) | An agent inside the cluster pulls from Git and applies | Strongest security boundary; continuous drift correction |
flowchart LR
DEV[Developer] -->|pull request| GIT[(Git: desired state)]
CI[CI pipeline] -->|updates image tag| GIT
AG[GitOps agent in cluster] -->|watches| GIT
AG -->|applies and reconciles| K[(Cluster)]
K -. drift detected .-> AG
Why GitOps is attractive:
- Git is the audit log and the rollback (git revert)
- No inbound cluster credentials in CI; the cluster reaches out
- Drift (manual edits) is detected and reverted
- Disaster recovery: point a new cluster at the same repo
Tools
| Tool | Notes |
|---|---|
| Argo CD | Popular, rich web UI, application and app-of-apps model |
| Flux CD | Toolkit of controllers, strong Helm, Kustomize and image automation |
| Kluctl | Diff-first CLI with a GitOps controller (Packaging and Multi-Environment Deploys) |
Repository layout that works: separate the application repo (code, Dockerfile, CI) from the environment repo (manifests per environment) so deploys do not trigger code builds and vice versa.
3. Cluster and Node Upgrades
Upgrade regularly to patch vulnerabilities and get new features. Versions are supported for a limited window, so skipping falls you out of support.
Managed services (GKE, EKS, AKS) can automate much of it: for example GKE offers release channels (rapid, regular, stable) and upgrades the control plane and nodes for you. You still decide when to upgrade early or manually, and you must make sure your workloads survive it. See 1. Orchestration Platforms.
A safe manual process
flowchart TD
A[1. Check for deprecated or removed APIs] --> B[2. Upgrade the control plane]
B --> C[3. Create a NEW node pool on the new version]
C --> D[4. Cordon and drain the old nodes]
D --> E[5. Verify workloads on the new nodes]
E --> F[6. Delete the old node pool]
- Deprecated APIs: find manifests using APIs the next version removes (tools such as
kubentorpluto, or the API server's deprecation metrics) and migrate them first - Control plane first: it may be ahead of the nodes by a limited skew, never behind
- New node pool: rather than upgrading nodes in place, add a fresh pool; the new nodes are proven before any workload moves, and rollback is just keeping the old pool
- Cordon and drain:
$ kubectl cordon <old-node> # stop new pods landing here
$ kubectl drain <old-node> --ignore-daemonsets --delete-emptydir-data
- Check workloads, probes and storage on the new nodes
- Delete the old pool
Making workloads upgrade-safe
| Need | Setting |
|---|---|
| Keep enough replicas up during drains | PodDisruptionBudget (minAvailable or maxUnavailable) |
| Spread across nodes and zones | topologySpreadConstraints or anti-affinity |
| Graceful shutdown | terminationGracePeriodSeconds, handle SIGTERM, preStop hook |
| No traffic to unready pods | Readiness probes |
| At least 2 replicas | Otherwise a drain means downtime |
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api }
spec:
minAvailable: 2
selector:
matchLabels: { app: api }
Before and after
- Back up cluster state and persistent data; test the upgrade in a non-production cluster first
- Read the release notes for removed features and changed defaults
- Upgrade add-ons (ingress controller, CNI, operators) to versions that support the new Kubernetes version
- Watch metrics and error rates after each step