Practice — GitOps & Continuous Delivery
Eight performance-based tasks for the single biggest slice of the CNPE blueprint — GitOps & Continuous Delivery is 25% of the exam, roughly one task in four on your paper. Drill them the way the exam will feel: cold, with only the official docs open and no worked solution in sight; time-boxed to five to seven minutes each, with a hard stop when the clock runs out; and open the answer key only after you have genuinely attempted the task on a real cluster. Peeking first turns a drill into reading, and reading is exactly what doesn't get you through a hands-on exam. This is one domain lifted out of the full practice bank — the background reading lives in GitOps workflows, CI/CD & progressive delivery and release engineering.
Imagine a robot who keeps your bedroom exactly like the picture you drew of it. These eight challenges are all about that robot: teach it to read your picture, teach it to fix things when your little brother moves a toy, teach it to look after four bedrooms at once, and — the tricky ones at the end — teach it to swap in new furniture a little at a time, so if the new chair is broken it can put the old one back before anyone notices.
☺ Like you’re 10: These tasks are all about writing what you want in a Git folder and getting a robot in the cluster to make it real — and then rolling out a new version slowly enough to catch a mistake.
The biggest domain, and the most mechanical — which is good news, because mechanical tasks are the ones you can drill to reflex. Expect to create Argo CD Application resources, restructure manifests with Kustomize, build a pipeline, and configure a progressive rollout. Keep the command reference beside you for the CLI shapes, and when a drill goes sideways, the troubleshooting playbook is faster than guessing.
Reconciling desired state — Argo CD, Kustomize and Flux
The four foundational drills. Each one is “make a controller inside the cluster hold reality to what Git says,” from a single app, through a properly structured repo, to many tenants at once, and finally the same job done with the other major reconciler.
G1 · Make an app self-heal against drift
Your platform team has just adopted Argo CD, and the payments team’s manifests already live in the platform config repo at apps/payments/overlays/prod. Right now someone still deploys them by hand, and last week an engineer scaled the deployment during an incident and never scaled it back — nobody noticed for four days.
Your task:
- Create an Argo CD
Applicationnamedpaymentsin theargocdnamespace pointing at branchmain, pathapps/payments/overlays/prodof the config repo, targeting namespacepaymentson the in-cluster destination. - Enable automated sync with both
pruneandselfHeal, and have Argo CD create the destination namespace. - Prove drift is corrected: scale the deployment by hand to 7 replicas and show Argo CD reverts it.
Done when: kubectl -n argocd get app payments -o jsonpath='{.status.sync.status}{" "}{.status.health.status}' prints Synced Healthy, and after a manual kubectl scale, kubectl -n payments get deploy api -o jsonpath='{.spec.replicas}' returns to the value in Git within a minute.
Show the worked solution
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/acme/platform-config.git
targetRevision: main
path: apps/payments/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true # delete live resources removed from Git
selfHeal: true # revert out-of-band drift back to Git
syncOptions:
- CreateNamespace=truekubectl apply -f payments-app.yaml
argocd app wait payments --health --timeout 120 # or watch the UI
# prove self-heal
kubectl -n payments scale deploy/api --replicas=7
sleep 60
kubectl -n payments get deploy api -o jsonpath='{.spec.replicas}' # back to Git's valueWhy: selfHeal is what turns Argo CD from a deploy button into a continuously enforcing controller — without it, drift is only reported, never corrected. prune makes deletions in Git into real deletions, which is the other half of “Git is the source of truth.” If the CLI is faster for you, argocd app create payments --repo … --path … --dest-namespace payments --sync-policy automated --auto-prune --self-heal --sync-option CreateNamespace=true produces the same object.
G2 · Convert a flat Deployment into a Kustomize base and overlays
The search team keeps three near-identical copies of their manifests in manifests/dev, manifests/staging and manifests/prod. The three have already drifted — prod is two image tags behind and nobody spotted it, because diffing three whole directories by eye is hopeless.
Your task:
- Create
base/containing the shareddeployment.yaml,service.yamland akustomization.yaml. - Create
overlays/prod/that sets the namespace tosearch-prod, raises replicas to 5, and pins the image toghcr.io/acme/search:2.3.1— using Kustomize fields, not a copied Deployment. - Render and apply the prod overlay.
Done when: kubectl kustomize overlays/prod emits a Deployment with 5 replicas and the pinned tag, and kubectl -n search-prod get deploy search -o jsonpath='{.spec.template.spec.containers[0].image}' shows ghcr.io/acme/search:2.3.1.
Show the worked solution
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/name: search
includeSelectors: true
---
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: search-prod
resources:
- ../../base
replicas:
- name: search
count: 5
images:
- name: ghcr.io/acme/search
newTag: "2.3.1"
patches:
- target: { kind: Deployment, name: search }
patch: |-
- op: add
path: /spec/template/spec/containers/0/resources
value: { requests: { cpu: 200m, memory: 256Mi }, limits: { memory: 512Mi } }kubectl kustomize overlays/prod # render only — always check before applying
kubectl apply -k overlays/prod
kubectl -n search-prod get deploy search -o jsonpath='{.spec.template.spec.containers[0].image}'Why: the replicas and images transformers exist precisely so an environment difference is a two-line diff instead of a duplicated file — that is the whole point of base/overlay. Reach for a strategic-merge or JSON6902 patches entry only for changes the built-in transformers can’t express. More in configuration management.
G3 · Fan one app across many namespaces with an ApplicationSet
Four tenant teams each need the same shared logging-agent deployed into their own namespace, with a per-tenant log level. Hand-writing four Application objects works today and breaks the moment a fifth team onboards.
Your task:
- Create an
ApplicationSetnamedlogging-agentusing a list generator with entries for tenantspayments,search,checkoutandidentity, each carrying alogLevel. - Template one Argo CD
Applicationper tenant, deploying into{{tenant}}, with automated sync and namespace creation. - Pass the per-tenant log level through as a Helm parameter (or Kustomize var).
Done when: kubectl -n argocd get applications lists four generated apps named logging-agent-<tenant>, all Synced, and each tenant namespace has the agent running.
Show the worked solution
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: logging-agent
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- list:
elements:
- tenant: payments
logLevel: info
- tenant: search
logLevel: debug
- tenant: checkout
logLevel: info
- tenant: identity
logLevel: warn
template:
metadata:
name: 'logging-agent-{{.tenant}}'
spec:
project: default
source:
repoURL: https://github.com/acme/platform-config.git
targetRevision: main
path: charts/logging-agent
helm:
parameters:
- name: logLevel
value: '{{.logLevel}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.tenant}}'
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ]Why: generators turn “N nearly identical Applications” into “one template plus a list of facts.” Swap the list generator for a git directory generator and onboarding a tenant becomes “create a folder”; swap in a cluster generator and the same app lands on every registered cluster. Note the older {{tenant}} fastjson syntax still appears widely — goTemplate: true uses {{.tenant}}, so match whichever style the exam environment’s docs show.
G4 · Reconcile the same app with Flux
A second cluster in your estate was set up by a different team and runs Flux rather than Argo CD. You need the checkout app reconciled there from the same config repo, with a ten-minute interval and pruning enabled — and you need to be able to force an immediate reconcile during an incident.
Your task:
- Create a
GitRepositorysource namedplatform-configinflux-systemtracking branchmain, polled every minute. - Create a
Kustomizationnamedcheckoutreconciling./apps/checkout/overlays/prodwithprune: trueandwait: true. - Force an out-of-band reconcile and confirm it succeeded.
Done when: flux get kustomizations checkout shows Ready=True with the applied revision, and flux reconcile kustomization checkout --with-source completes without error.
Show the worked solution
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: platform-config
namespace: flux-system
spec:
interval: 1m
url: https://github.com/acme/platform-config.git
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: checkout
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: platform-config
path: ./apps/checkout/overlays/prod
targetNamespace: checkout
prune: true
wait: true
timeout: 3m# the CLI equivalents — much faster under exam time pressure flux create source git platform-config \ --url=https://github.com/acme/platform-config.git --branch=main --interval=1m flux create kustomization checkout \ --source=GitRepository/platform-config --path=./apps/checkout/overlays/prod \ --prune=true --wait=true --interval=10m --target-namespace=checkout flux reconcile kustomization checkout --with-source flux get kustomizations checkout
Why: Flux splits “where the manifests come from” (GitRepository) from “what to do with them” (Kustomization/HelmRelease), so one source can feed many reconcilers. --with-source matters during incidents: without it, flux reconcile re-applies the last fetched revision rather than pulling your new commit.
Run G1 through G4 back-to-back as one 25-minute block, on a fresh kind cluster, with a kitchen timer set to six minutes per task. When the timer goes, stop mid-keystroke — flag it, write one line about where you got stuck, and start the next task. Only when all four are done (or abandoned) do you open a single answer key. Then re-run every task you flagged, from scratch, on a clean cluster. The second pass is where the reflex is actually built; the first pass only tells you which reflexes are missing. Setup script and cluster recipe live in the lab track.
Building the pipeline — Tekton and Argo Workflows
GitOps says what should be running; something still has to build the artefact and run the housekeeping. These two drills cover the Kubernetes-native pipeline engines the exam expects you to author by hand, not just invoke.
G5 · Build a Tekton Task and Pipeline, then run it
The platform’s golden-path pipeline has to build a container image from a Git repo and push it to the internal registry. There is no pipeline yet; you have Tekton Pipelines installed and a service account with registry credentials attached.
Your task:
- Write a Tekton
Tasknamedbuild-pushthat takes animageparameter and asourceworkspace, and builds/pushes with Buildah (or Kaniko). - Write a
Pipelinenamedapp-cithat runsgit-clonethenbuild-push, passing the workspace between them. - Start it with
tknand confirm thePipelineRunsucceeds.
Done when: tkn pipelinerun list shows the run Succeeded, and tkn pipelinerun logs -f shows the push step completing.
Show the worked solution
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: build-push
spec:
params:
- name: image
type: string
workspaces:
- name: source
steps:
- name: build-and-push
image: quay.io/buildah/stable:latest
workingDir: $(workspaces.source.path)
securityContext:
capabilities:
add: ["SETFCAP"]
script: |
#!/usr/bin/env bash
set -euo pipefail
buildah bud --storage-driver=vfs -t "$(params.image)" .
buildah push --storage-driver=vfs "$(params.image)"
---
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: app-ci
spec:
params:
- name: repo-url
- name: image
workspaces:
- name: shared
tasks:
- name: fetch
taskRef: { name: git-clone } # from Tekton Hub / cluster catalog
params:
- name: url
value: $(params.repo-url)
workspaces:
- name: output
workspace: shared
- name: build
runAfter: ["fetch"]
taskRef: { name: build-push }
params:
- name: image
value: $(params.image)
workspaces:
- name: source
workspace: sharedkubectl apply -f task.yaml -f pipeline.yaml tkn pipeline start app-ci \ --param repo-url=https://github.com/acme/checkout.git \ --param image=registry.internal/acme/checkout:1.0.0 \ --workspace name=shared,volumeClaimTemplateFile=pvc.yaml \ --serviceaccount build-bot --use-param-defaults --showlog
Why: Tekton’s unit of reuse is the Task; a Pipeline only wires Tasks together and hands them shared workspaces (a PVC, ConfigMap or Secret mounted into each step). runAfter gives you explicit ordering. Under exam time pressure, tkn pipeline start --showlog is much quicker than hand-writing a PipelineRun, and it prints the failure immediately if something is wrong.
G6 · Model a fan-out/fan-in job as an Argo Workflow DAG
Nightly platform housekeeping currently runs as one long shell script in a CronJob: it collects inventory from three sources, then produces a single report once all three finish. When one source is slow, everything is slow, and when one fails you can’t tell which.
Your task:
- Write an Argo
Workflowusing adagtemplate with three parallel collection steps and one report step that depends on all three. - Pass a parameter into each collection step so they share one template.
- Submit it and watch it to completion.
Done when: argo get @latest shows the three collect nodes running in parallel and report only after all three, with phase Succeeded.
Show the worked solution
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: inventory-
spec:
entrypoint: nightly
templates:
- name: nightly
dag:
tasks:
- name: collect-clusters
template: collect
arguments: { parameters: [{ name: source, value: clusters }] }
- name: collect-images
template: collect
arguments: { parameters: [{ name: source, value: images }] }
- name: collect-costs
template: collect
arguments: { parameters: [{ name: source, value: costs }] }
- name: report
template: report
dependencies: [collect-clusters, collect-images, collect-costs]
- name: collect
inputs:
parameters: [{ name: source }]
container:
image: alpine:3.20
command: [sh, -c]
args: ["echo collecting {{inputs.parameters.source}}; sleep 5"]
- name: report
container:
image: alpine:3.20
command: [sh, -c]
args: ["echo all sources collected - writing report"]argo submit -n argo workflow.yaml --watch argo get -n argo @latest argo logs -n argo @latest
Why: a dag expresses dependencies rather than sequence, so anything without a dependencies entry runs in parallel automatically — that is the entire win over a shell script. A steps template is the alternative: sequential by default, with parallelism expressed by nesting list items.
Progressive delivery — canaries that abort themselves
The last two drills are the highest-value ones in the domain, because they are where candidates most often lose marks: knowing the manifest shape is not enough, you also have to know which command promotes, which aborts, and what each leaves behind. Read CI/CD & progressive delivery first if the vocabulary is shaky.
G7 · Turn a Deployment into an analysed Argo Rollouts canary
Checkout is your highest-risk service and currently deploys with a plain rolling update — a bad image reaches 100% of users in about forty seconds. The team wants a canary that shifts traffic in stages and automatically aborts if the error rate rises.
Your task:
- Convert the
checkoutDeployment into an ArgoRolloutwith a canary strategy: 20% → pause 2m → 50% → pause → 100%. - Add an
AnalysisTemplatethat queries Prometheus for success rate and fails below 95%, and wire it into the canary steps. - Trigger an update, promote through a pause, then trigger a second update and abort it.
Done when: kubectl argo rollouts get rollout checkout --watch shows the canary paused at 20%; after promote it advances; and after abort the rollout reports Degraded with all traffic back on stable.
Show the worked solution
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service
metrics:
- name: success-rate
interval: 30s
count: 4
successCondition: result[0] >= 0.95
failureLimit: 1
provider:
prometheus:
address: http://prometheus-operated.monitoring:9090
query: |
sum(rate(http_requests_total{service="{{args.service}}",code!~"5.."}[2m]))
/
sum(rate(http_requests_total{service="{{args.service}}"}[2m]))
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: checkout
spec:
replicas: 6
selector:
matchLabels: { app: checkout }
template: # identical to your Deployment's pod template
metadata:
labels: { app: checkout }
spec:
containers:
- name: app
image: ghcr.io/acme/checkout:2.0.0
strategy:
canary:
canaryService: checkout-canary
stableService: checkout-stable
analysis:
templates: [{ templateName: success-rate }]
args: [{ name: service, value: checkout }]
steps:
- setWeight: 20
- pause: { duration: 2m }
- setWeight: 50
- pause: {} # empty pause = wait for a human promote
- setWeight: 100kubectl argo rollouts get rollout checkout --watch kubectl argo rollouts set image checkout app=ghcr.io/acme/checkout:2.1.0 kubectl argo rollouts promote checkout # advance past an indefinite pause kubectl argo rollouts promote checkout --full # skip all remaining steps + analysis kubectl argo rollouts abort checkout # send 100% back to stable kubectl argo rollouts undo checkout --to-revision=2
Why: the steps list is the traffic plan and the analysis block is the automatic verdict — pause: {} with no duration waits forever for a human, which is exactly what a manual gate looks like. Know the difference between abort (stop and return traffic to stable, rollout goes Degraded) and undo (roll back to an earlier revision): the exam wording usually implies one specifically.
G8 · Configure a Flagger canary with automatic rollback
A different team already runs Linkerd (or Istio) and would rather not adopt a second rollout controller. Flagger is installed. They want the same protection as G7 but driven from their existing Deployment, with automatic promotion when metrics stay healthy.
Your task:
- Create a Flagger
Canarytargeting thefrontendDeployment and its Service on port 8080. - Step traffic 10% at a time up to 50%, analysing every 30 seconds with a request-success-rate threshold of 99% and a p99 latency threshold of 500 ms.
- Trigger a rollout and watch Flagger promote it — then push a deliberately broken image and watch it roll back.
Done when: kubectl -n prod get canary frontend shows Succeeded in the STATUS column after a good release, and Failed with traffic returned to primary after the bad one; kubectl -n prod describe canary frontend shows the halt reason in events.
Show the worked solution
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: frontend
namespace: prod
spec:
provider: linkerd
targetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
service:
port: 8080
targetPort: 8080
analysis:
interval: 30s
threshold: 5 # consecutive failed checks before rollback
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange: { min: 99 }
interval: 1m
- name: request-duration
thresholdRange: { max: 500 }
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
cmd: "hey -z 1m -q 10 -c 2 http://frontend-canary.prod:8080/"kubectl -n prod set image deploy/frontend app=ghcr.io/acme/frontend:3.1.0 kubectl -n prod get canary frontend -w kubectl -n prod describe canary frontend | tail -20 # events show each weight step
Why: Flagger’s model is the mirror image of Argo Rollouts — you keep your ordinary Deployment and Flagger clones it into primary/canary and drives the mesh’s traffic split. The webhooks load-tester matters more than it looks: a canary with no traffic generates no metrics, so the analysis has nothing to judge and stalls.
1. Which two Argo CD syncPolicy.automated fields did G1 turn on, and what does each one actually do? 2. In G2, which two Kustomize transformers let an environment difference be a two-line diff instead of a copied Deployment? 3. In an ApplicationSet with goTemplate: true, what does the templating syntax look like, and how does it differ from the older style? 4. Why does flux reconcile kustomization without --with-source often fail to pick up the commit you just pushed? 5. What is Tekton’s unit of reuse, and what mechanism passes data between two Tasks in a Pipeline? 6. What is the difference between kubectl argo rollouts abort and undo? 7. Why does a Flagger canary stall if you omit the load-tester webhook?
Check your answers
prunedeletes live resources that have been removed from Git;selfHealreverts out-of-band drift back to what Git says. WithoutselfHeal, drift is only reported, never corrected.- The
replicasandimagestransformers. Fall back to a strategic-merge or JSON6902patchesentry only for changes the built-in transformers can’t express. goTemplate: trueuses Go template syntax —{{.tenant}}with a leading dot; the older fastjson style is{{tenant}}. Both still appear widely, so match whichever the exam environment’s docs show.- Because without
--with-sourceit re-applies the last fetched revision rather than pulling the repo again first. During an incident that quietly re-applies the old commit. - The
Taskis the unit of reuse; aPipelineonly wires Tasks together. Data passes between them through shared workspaces (a PVC, ConfigMap or Secret mounted into each step), withrunAftergiving explicit ordering. - abort stops the in-flight rollout and shifts all traffic back to the stable ReplicaSet, leaving the Rollout
Degraded. undo rolls back to a previous revision, creating a new rollout to get there. - A canary receiving no traffic produces no metrics, so the analysis has nothing to judge and never reaches a verdict. The load-tester generates the requests the success-rate and latency checks measure.
That’s the GitOps domain drilled. Anything that scored badly, re-do cold on a clean cluster before you move on — then head back to the full practice bank for the other four domains and the 120-minute mock. For the theory underneath these drills, see GitOps workflows and release engineering; for the CLI flags you fumbled, the command reference; and when a drill breaks in a way you don’t recognise, work it through the troubleshooting playbook or delivery triage rather than reaching straight for the answer key.