Exam Prep · Know It Cold

Know It Cold — the manifests you must write from memory

The CNPE is not a broadly open-book exam. While the clock runs you get kubernetes.io/docs, kubernetes.io/blog, whatever the exam links from its own Quick Reference box, and the documentation installed locally on the machine — and nothing else. Argo CD, Flux, Tekton, Argo Rollouts, Crossplane, Prometheus, OpenTelemetry, Kyverno, Gatekeeper, Istio, Helm and Backstage are all squarely inside the exam domains, and not one of their doc sites is on that list. So every manifest on this page has to come out of your head. That is the whole premise: this is the half of the exam kubernetes.io cannot carry for you. Drill it the way it will be tested — blank page, empty file, no browser, no notes — and then check yourself against the real docs while you still can. Re-reading this page feels like learning and is not; typing these skeletons from nothing is. If you can only do one exercise between now and exam day, make it the one at the bottom of this page.

☺ Explain it like I’m 10

Imagine a cooking test where you may bring one big book about ingredients and ovens — but none of the little recipe cards for the fancy dishes. The fancy dishes are still on the menu. So the recipes have to live in your head, because nobody is handing them to you on the day. This page is those recipes. Don’t read them like a story. Close the page, write one out on a blank sheet, then come back and see what you got wrong.

🐰🦋Your hosts for this topic: Remy the Rabbit & Mira the Butterfly — Remy is pure speed and recall, the one who can type a whole Rollout before you’ve found the tab; Mira knows what each of these APIs is actually for, so the shapes stick as meaning rather than as letters. Remy drills you; Mira makes sure you understand what you’re drilling.
⚠ Why this page exists — the CNPE allowlist is narrow

During the CNPE the only permitted resources are: https://kubernetes.io/docs including its translations, https://kubernetes.io/blog/, task-specific documentation linked from the exam’s own “Quick Reference” box, and documentation installed locally on the exam machine — man pages, /usr/share, distribution packages.

The doc sites for Argo CD, Flux, Tekton, Argo Rollouts, Crossplane, Prometheus, OpenTelemetry, Kyverno, Gatekeeper, Istio, Helm and Backstage are NOT available to you, even though every one of them sits inside a named exam domain. You may use the search box on a permitted site, but you may not open external search results and you may not follow outbound links off a permitted site — a kubernetes.io page linking to helm.sh does not make helm.sh permitted.

Allowlists get revised, and this page is not an authority. Verify the current list yourself on the official Linux Foundation Resources Allowed page and the Important Instructions: CNPE page in the days before your exam, and let those override anything written here. The Docs Map covers the rules, the links and the kubernetes.io half in full.

How to use this page

☺ Like you’re 10: You don’t need every field. You need the shape: the right first two lines, and the two or three fields that actually mean something.

You do not need to memorise every optional field of every CRD — nobody does that, and the exam does not reward it. You need the skeleton: the correct apiVersion and kind, the two or three fields that carry the meaning, and the nesting that holds them together. Get that much onto the page and the cluster itself will fill in the rest. Everything below is written to that standard: enough to be correct and applyable, with the annotations marking the exact spots where marks are usually lost.

Read each block once for shape, then close the page. Open an empty file. Type it. Compare. The gap you find is your actual study list — and it will be shorter and more specific than you fear. Anything you get wrong twice belongs on a flashcard, not on a re-read.

The three routes to a spec on exam day

There are exactly three, and you control only the first. Memory — this page. The Quick Reference box — if a task involves a tool whose docs are closed, the exam may link the specific page you need directly from the task; it is a permitted resource and it is targeted, so make reading it step zero of every task. kubectl explain — once a CRD is installed in the cluster its full OpenAPI schema is in the cluster, and the docs come to you with no browser at all.

◆ The recovery move when memory fails

kubectl explain <kind> --recursive is your safety net for every resource on this page, because installed CRDs register their full schema in the cluster. kubectl api-resources | grep -i rollout gives you the kind and its apiVersion; kubectl explain rollout.spec.strategy.canary --recursive gives you the field tree, version-correct by construction. Also check whether an example already exists in the cluster — kubectl get application -n argocd -o yaml on an existing app is a working reference manifest you were handed for free. Reading the cluster is always permitted. More of these in the command reference.

Delivery — Argo CD, Flux, Tekton and Argo Rollouts

☺ Like you’re 10: These are the robots that take what’s written in Git and make it real. Four tools, six shapes.

This is the exam’s largest domain and the one whose specs you are most likely to type unaided. Background reading lives in GitOps Workflows; what follows is the memory layer on top of it.

Argo CD — Application

The Argo CD Application is the single most likely thing you will write without a reference. Know source (repoURL, path, targetRevision), destination (server or name, plus namespace), project, and the syncPolicy.automated block — remembering that prune and selfHeal both default to false, so “auto-sync” alone does not delete removed resources or revert drift. CreateNamespace=true lives in syncOptions, not in automated.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web
  namespace: argocd                 # Applications live in the Argo CD namespace
spec:
  project: default
  source:
    repoURL: https://github.com/org/repo.git
    targetRevision: main            # branch, tag or commit SHA
    path: envs/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: prod
  syncPolicy:
    automated:
      prune: true                   # default false — delete removed resources
      selfHeal: true                # default false — revert manual drift
    syncOptions:
    - CreateNamespace=true          # NOT under automated:

Argo CD — ApplicationSet

For ApplicationSet, memorise the wrapper and at least the List and Git generators; Matrix and Cluster are worth recognising. The shape is always spec.generators[] plus a spec.template that is an Application with {{ }} placeholders filled from the generator.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: {name: tenants, namespace: argocd}
spec:
  generators:
  - git:                            # one App per directory in the repo
      repoURL: https://github.com/org/repo.git
      revision: main
      directories:
      - path: tenants/*
  # - list: {elements: [{cluster: dev, url: https://dev.example}]}
  # - clusters: {}                  # one App per registered cluster
  # - matrix: {generators: [ ... , ... ]}   # cross-product of two
  # goTemplate: true               # then placeholders become '{{.path.basename}}'
  template:
    metadata: {name: '{{path.basename}}'}
    spec:
      project: default
      source: {repoURL: https://github.com/org/repo.git, targetRevision: main, path: '{{path}}'}
      destination: {server: https://kubernetes.default.svc, namespace: '{{path.basename}}'}

Flux — GitRepository + Kustomization

For Flux, the pairing to know is GitRepository (the source) plus Kustomization (the applier), and the fact that they live in different API groups — source.toolkit.fluxcd.io and kustomize.toolkit.fluxcd.io. Both need an interval. The Kustomization’s prune and wait are the fields tasks care about.

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata: {name: app, namespace: flux-system}
spec:
  interval: 1m
  url: https://github.com/org/repo.git
  ref: {branch: main}               # or tag: / semver: / commit:
  # secretRef: {name: git-creds}    # private repos
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: {name: app, namespace: flux-system}
spec:
  interval: 10m
  sourceRef: {kind: GitRepository, name: app}
  path: ./envs/prod
  prune: true                       # garbage-collect removed resources
  wait: true                        # block until resources are Ready
  targetNamespace: prod
  # dependsOn: [{name: infra}]

Tekton — Task, Pipeline, PipelineRun

For Tekton, know the three-object chain: a Task holds steps; a Pipeline references Tasks via taskRef and orders them with runAfter; a PipelineRun executes it and is where workspaces get bound to real storage. Workspaces are the classic stumble — they must be declared in the Task, declared and mapped in the Pipeline, and bound in the PipelineRun. Miss any of the three and it fails.

apiVersion: tekton.dev/v1
kind: Task
metadata: {name: build}
spec:
  workspaces:
  - name: source                    # ① declared in the Task
  params:
  - name: image
    type: string
  steps:
  - name: build
    image: gcr.io/kaniko-project/executor:latest
    script: |
      echo building $(params.image) from $(workspaces.source.path)
---
apiVersion: tekton.dev/v1
kind: Pipeline
metadata: {name: ci}
spec:
  workspaces:
  - name: shared                    # ② declared in the Pipeline
  params:
  - name: image
    type: string
  tasks:
  - name: fetch
    taskRef: {name: git-clone}
    params:
    - name: url                     #    git-clone's url param has no default
      value: https://github.com/org/repo.git
    workspaces:
    - name: output                  #    name: = the workspace the TASK declares
      workspace: shared             #    workspace: = the Pipeline's own name
  - name: build
    runAfter: [fetch]               #    ordering
    taskRef: {name: build}
    params:
    - name: image
      value: $(params.image)
    workspaces:
    - name: source
      workspace: shared
---
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: {generateName: ci-run-}
spec:
  pipelineRef: {name: ci}
  params:
  - name: image
    value: registry.example/app:v1
  workspaces:
  - name: shared                    # ③ BOUND to real storage here
    persistentVolumeClaim: {claimName: ci-cache}
    # or: volumeClaimTemplate: / emptyDir: {}

Argo Rollouts — Rollout + AnalysisTemplate

For Argo Rollouts, the key mental model is that a Rollout replaces your Deployment — same spec.template, different kind — and that canary steps is an ordered list where a pause with no duration waits indefinitely for a manual promote. An AnalysisTemplate gates the rollout on a metric; the successCondition is what decides pass or fail. Do not confuse this with Flagger, which keeps your Deployment and wraps a Canary CR around it — copying the wrong tool’s example applies cleanly and does nothing useful.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: {name: web}
spec:
  replicas: 5
  selector: {matchLabels: {app: web}}
  template:                         # identical to a Deployment's pod template
    metadata:
      labels: {app: web}            # must match spec.selector.matchLabels
    spec:
      containers:
      - name: web
        image: registry.example/app:v1
  strategy:
    canary:
      canaryService: web-canary
      stableService: web-stable
      steps:
      - setWeight: 20
      - pause: {duration: 60s}      # bare `- pause: {}` waits for manual promote
      - analysis:
          templates:
          - templateName: success-rate
      - setWeight: 50
      - pause: {duration: 60s}
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata: {name: success-rate}
spec:
  metrics:
  - name: success-rate
    interval: 1m
    count: 5
    successCondition: result[0] >= 0.95
    failureLimit: 2
    provider:
      prometheus:
        address: http://prometheus.monitoring:9090
        query: |
          sum(rate(http_requests_total{status!~"5.."}[2m]))
          / sum(rate(http_requests_total[2m]))

Platform APIs — Crossplane & CRDs

☺ Like you’re 10: Crossplane has two YAMLs that look nearly the same — the definition the platform team writes, and the claim the developer writes. Check which one the task wants.

The self-service domain, and the one candidates most often garble under pressure. Concepts in Platform APIs & CRDs and Self-Service; the shapes here.

Crossplane — XRD, Composition, and the claim

Crossplane’s three-object story: the XRD (CompositeResourceDefinition) defines the developer-facing API and its claimNames; the Composition says what real infrastructure that API maps to; the claim is the small namespaced YAML a developer writes, using the claim kind, not the X-prefixed composite kind. Know which of the three a task is asking for — that alone is half the marks. The XRD’s metadata.name must be plural.group, and referenceable: true on the served version is what lets a Composition bind to it. One version caveat worth carrying in: Crossplane has been moving composition logic out of the inline spec.resources patch-and-transform block below and into composition functionsspec.mode: Pipeline with a pipeline of functionRef steps. Both shapes appear in the wild, so let the cluster settle it: kubectl explain composition.spec tells you which fields the installed version actually serves, and kubectl api-resources tells you whether the claim kind exists.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata: {name: xpostgresqlinstances.platform.example.org}   # plural.group
spec:
  group: platform.example.org
  names: {kind: XPostgreSQLInstance, plural: xpostgresqlinstances}
  claimNames: {kind: PostgreSQLInstance, plural: postgresqlinstances}
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              size: {type: string, enum: [small, large]}
            required: [size]
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata: {name: postgres-aws}
spec:
  compositeTypeRef: {apiVersion: platform.example.org/v1alpha1, kind: XPostgreSQLInstance}
  # inline patch-and-transform; newer Crossplane prefers
  # `mode: Pipeline` + a pipeline of functionRef steps
  resources:
  - name: rds
    base:
      apiVersion: rds.aws.upbound.io/v1beta1
      kind: Instance
      spec: {forProvider: {region: eu-west-1}}
    patches:
    - fromFieldPath: spec.size
      toFieldPath: spec.forProvider.instanceClass
      transforms:
      - type: map
        map: {small: db.t3.micro, large: db.m5.large}
---
# What the DEVELOPER writes — namespaced, uses the CLAIM kind:
apiVersion: platform.example.org/v1alpha1
kind: PostgreSQLInstance
metadata: {name: orders-db, namespace: team-a}
spec:
  size: small

The CRD — the one platform API whose docs you do get

Worth noting the exception on this page: a plain CustomResourceDefinition is Kubernetes-native, so it is documented on kubernetes.io under Tasks → Extend Kubernetes, and you may open that page during the exam. Even so, learn the shape — versions[].schema.openAPIV3Schema, subresources: {status: {}, scale: {}}, additionalPrinterColumns, scope and names.shortNames — because retrieving it still costs you thirty seconds you may not have, and because the XRD above is essentially a CRD wearing a Crossplane hat.

Observability — Prometheus & OpenTelemetry

☺ Like you’re 10: Two shapes: one that says “go collect numbers from this app,” and one that says “here’s where the numbers should flow.”

The collection half of observability tasks is nearly always a copyable manifest — except that here you have nothing to copy from. Concepts in Observability.

Prometheus — ServiceMonitor and PrometheusRule

The ServiceMonitor trips people on two details: endpoints[].port takes the named port from the Service (not a number), and the Prometheus instance only picks up ServiceMonitors matching its own serviceMonitorSelector — usually a release label. A PrometheusRule is the alerting-rules file wrapped in a CRD; the for duration and labels.severity are what alerting tasks check.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: web
  namespace: monitoring
  labels: {release: kube-prometheus-stack}   # must match serviceMonitorSelector
spec:
  selector: {matchLabels: {app: web}}        # selects the SERVICE, not pods
  namespaceSelector: {matchNames: [prod]}
  endpoints:
  - port: metrics                            # the Service's NAMED port
    path: /metrics
    interval: 30s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: web-alerts
  namespace: monitoring
  labels: {release: kube-prometheus-stack}
spec:
  groups:
  - name: web
    rules:
    - alert: HighErrorRate
      expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05
      for: 10m
      labels: {severity: critical}
      annotations: {summary: "Error rate above 5% for 10m"}

OpenTelemetry — the Collector config

For the OpenTelemetry Collector, memorise the four-block config and — the part everyone forgets — that declaring receivers, processors and exporters does nothing until service.pipelines wires them together. A config with a perfect receivers block and no pipeline is valid YAML that moves zero telemetry. Know the OTLP ports cold: 4317 for gRPC, 4318 for HTTP.

receivers:
  otlp:
    protocols:
      grpc: {endpoint: 0.0.0.0:4317}   # OTLP/gRPC
      http: {endpoint: 0.0.0.0:4318}   # OTLP/HTTP
processors:
  batch: {}
  memory_limiter: {check_interval: 1s, limit_percentage: 80, spike_limit_percentage: 25}
exporters:
  otlp: {endpoint: jaeger-collector:4317, tls: {insecure: true}}
  debug: {verbosity: detailed}
service:
  pipelines:                            # ← without this, nothing flows
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Security — Kyverno, Gatekeeper & Istio

☺ Like you’re 10: Rules that say no, and encryption between services. Both policy engines default to not blocking, which is the trap.

Both major policy engines ship enormous libraries of ready-made policies, and neither library is reachable during the exam — so the shapes have to be yours. Concepts in Security & Policy Enforcement.

Kyverno — ClusterPolicy

For Kyverno, know the ClusterPolicy skeleton and the one field that decides whether it does anything: validationFailureAction must be Enforce to block (it defaults to Audit, which logs the violation and lets the bad pod through). Know both rule shapes — validate with a pattern, and mutate with patchStrategicMerge. Newer Kyverno releases moved this switch down onto the rule as spec.rules[].validate.failureAction and treat the policy-level field as the legacy spelling, so confirm which one the installed version wants with kubectl explain clusterpolicy.spec.rules.validate before you commit to a shape.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: require-limits}
spec:
  validationFailureAction: Enforce      # default Audit = logs only, blocks nothing
  # newer Kyverno: per-rule `validate: {failureAction: Enforce}` instead
  background: true
  rules:
  - name: check-limits
    match:
      any:
      - resources: {kinds: [Pod]}
    validate:
      message: "CPU and memory limits are required."
      pattern:
        spec:
          containers:
          - resources:
              limits:
                memory: "?*"            # ?* = any non-empty value
                cpu: "?*"
  - name: add-team-label                # mutate rule in the same policy
    match:
      any:
      - resources: {kinds: [Pod]}
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            team: "{{request.object.metadata.namespace}}"

Gatekeeper — ConstraintTemplate + Constraint

For Gatekeeper, the pair is always ConstraintTemplate (the reusable Rego + parameter schema) plus a Constraint whose kind is the template’s crd.spec.names.kind. Two details worth burning in: the Rego rule head inside a ConstraintTemplate is violation[{"msg": msg}], not deny (that is standalone OPA); and enforcementAction defaults to deny but is frequently set to dryrun in examples you half-remember. Rego has since grown a stricter dialect in which the same head is written violation contains {"msg": msg} if { … }; if a cluster you are handed rejects the classic form, kubectl get constrainttemplate -o yaml on anything already installed shows you which dialect that Gatekeeper build expects.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: {name: k8srequiredlabels}
spec:
  crd:
    spec:
      names: {kind: K8sRequiredLabels}      # ← becomes the Constraint's kind
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels: {type: array, items: {type: string}}
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8srequiredlabels
      violation[{"msg": msg}] {           # NOT deny[...]
        required := input.parameters.labels[_]
        not input.review.object.metadata.labels[required]
        msg := sprintf("missing required label: %v", [required])
      }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels                    # the template's names.kind
metadata: {name: ns-must-have-owner}
spec:
  enforcementAction: deny                  # dryrun = logs only
  match:
    kinds:
    - apiGroups: [""]
      kinds: [Namespace]
  parameters:
    labels: [owner]

Istio — PeerAuthentication and AuthorizationPolicy

For Istio, two resources cover most of the security domain. PeerAuthentication with mtls.mode: STRICT enforces mTLS — mesh-wide if it lives in the Istio root namespace with no selector, namespace-scoped otherwise. AuthorizationPolicy controls who may call what; remember that an empty rule with action: DENY is the deny-all idiom, and that principals take SPIFFE identities of the form cluster.local/ns/<ns>/sa/<serviceaccount>.

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: prod                    # istio-system + no selector = mesh-wide
spec:
  mtls:
    mode: STRICT                      # PERMISSIVE accepts plaintext too
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: allow-frontend, namespace: prod}
spec:
  selector: {matchLabels: {app: orders}}
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/prod/sa/frontend"]
    to:
    - operation:
        methods: [GET, POST]
        paths: ["/api/*"]
---
# Deny-all idiom: empty spec.rules with action DENY
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: deny-all, namespace: prod}
spec:
  action: DENY
  rules:
  - {}

The three fields people forget

☺ Like you’re 10: Most lost marks aren’t a whole missing recipe. They’re one missing pinch of salt. Here are the usual three per dish.

If you only revise one thing on this page in the last hour before the exam, revise this table. Every row is a manifest that applies cleanly and scores nothing when the named fields are missing or wrong.

ResourceWhich toolThe 3 fields people forget
ApplicationArgo CDsyncPolicy.automated.prune and .selfHeal (both default false); syncOptions: [CreateNamespace=true] — a sibling of automated, not a child; metadata.namespace: argocd
ApplicationSetArgo CDspec.template (the generator alone produces nothing); the {{ }} placeholders that consume the generator’s output; generators[].git.directories[].path globbing
GitRepository + KustomizationFluxTwo different API groups — source.toolkit vs kustomize.toolkit; interval on both; the Kustomization’s prune and wait
Task / Pipeline / PipelineRunTektonWorkspaces wired in all three places — declared in the Task, declared and mapped in the Pipeline, bound to storage in the PipelineRun; runAfter for ordering; taskRef vs inline taskSpec
RolloutArgo RolloutscanaryService and stableService (both, named separately); a bare - pause: {} waits forever vs {duration: 60s}; it replaces the Deployment — do not leave both
AnalysisTemplateArgo RolloutssuccessCondition (no condition = no gate); failureLimit; the - analysis: step that actually references the template
XRD / Composition / claimCrossplaneXRD metadata.name must be plural.group; referenceable: true on the served version; the claim uses the claimNames kind, not the X-prefixed one
ServiceMonitorPrometheus OperatorThe release label matching the Prometheus serviceMonitorSelector; endpoints[].port is the Service’s named port, not a number; namespaceSelector when the Service is elsewhere
PrometheusRulePrometheus Operatorfor: (fires instantly without it); labels.severity for routing; the same release label as above
Collector configOpenTelemetryservice.pipelines — nothing flows without it; ports 4317 gRPC / 4318 HTTP; every component named in a pipeline must also be defined above
ClusterPolicyKyvernovalidationFailureAction: Enforce (Audit, the default, blocks nothing — newer Kyverno spells it per-rule as validate.failureAction); match.any.resources.kinds; the "?*" anchor meaning “any non-empty value”
ConstraintTemplate + ConstraintGatekeeperRego head is violation[{"msg": msg}], not deny; the Constraint’s kind is the template’s crd.spec.names.kind; enforcementAction: deny vs dryrun
PeerAuthenticationIstiomtls.mode: STRICT vs PERMISSIVE; scope comes from namespace + selector; root namespace with no selector = mesh-wide
AuthorizationPolicyIstioaction (defaults to ALLOW); the deny-all idiom rules: [{}] with action: DENY; SPIFFE principals of the form cluster.local/ns/<ns>/sa/<sa>

Drill it — blank page, no notes

☺ Like you’re 10: Reading the recipes again feels like studying. Writing one out with the book shut is studying. Do the second one.

Recognition and recall are different skills, and only one of them is tested. You will recognise every block on this page after one read; that feeling of fluency is exactly the illusion that sinks candidates at minute twenty. The only honest measure is an empty file.

🐰 Remy’s workshop · 30 min

The blank-page drill, and it is the most important exercise on this site. Open an empty file with no browser and no notes. Write, from memory: an Argo CD Application with automated prune and self-heal; a Flux GitRepository + Kustomization pair; a Tekton Task + Pipeline + PipelineRun sharing one workspace; an Argo Rollouts Rollout with two canary steps and an AnalysisTemplate; a Crossplane XRD, Composition and claim; a ServiceMonitor and a PrometheusRule; an OTel Collector config with a complete traces pipeline; a Kyverno ClusterPolicy that enforces; a Gatekeeper ConstraintTemplate + Constraint; and an Istio PeerAuthentication STRICT plus an AuthorizationPolicy. Then check each against the real project docs — now, while you still can. Anything you got wrong twice goes on a flashcard. Then build them for real in the practice tasks, and when one won’t come up healthy, work the troubleshooting playbook.

Run the drill three times across your final fortnight, not once. The first pass tells you what you don’t know; the second tells you what didn’t stick; by the third the skeletons come out without deliberation, which is the state you actually need — because on the day the manifest is the easy part of the task, and every second you spend recalling it is a second not spent reading the requirement properly, adjusting the name and namespace, and verifying the result.

🎬 At the Platform Guild
🐰

Remy: Blank file. Ninety seconds. Argo CD Application, prune and self-heal on. Go.

👺

Gizmo: Easy. syncPolicy.automated.prune, selfHeal, and createNamespace right underneath. Ship it! 🤑

🦋

Mira: Two of three. CreateNamespace=true is a syncOption, not something automated knows about. Ask what each field is for: automated answers “when do I sync,” syncOptions answers “how do I apply.” Different questions, different homes.

👺

Gizmo: Fine, I’ll just look it up on the day.

🦊

Foxy: On what? argo-cd.readthedocs.io isn’t on the allowlist. You get kubernetes.io, the blog, the Quick Reference box and man pages. That’s the list.

🤖

Recon: BEEP. Partial recovery available. kubectl explain application.spec.syncPolicy --recursive. The schema is in the cluster. The cluster is permitted.

🐢

Timmy: Which only helps if you remember the kind is called Application and it lives in argocd. explain fills in fields. It does not fill in names.

🦆

Dot: At my job nobody remembers YAML, we remember which tab. Turns out the exam is stricter than my job.

🐰

Remy: Then we drill. Blank file. Again. This time the Tekton three-place workspace — and Gizmo, all three places.

That is the page. Eleven skeletons, a table of the fields that quietly cost marks, and one drill that turns reading into recall. Pair it with the Docs Map for the kubernetes.io half you can look up, the exam guide for the logistics and the rules, the field notes for what people say it actually feels like, the command reference for the muscle memory around these manifests, and the glossary when a kind’s name is the bit that won’t come. Then close all of them and open an empty file.

🐢 Timmy’s checkpoint

1. In an Argo CD Application, where does CreateNamespace=true live — and what do prune and selfHeal default to? 2. Name the three places a Tekton workspace must appear before a PipelineRun will work. 3. Which Crossplane object does a developer write, and which kind does it use? 4. You wrote a perfect OTel Collector with receivers, processors and exporters, and no telemetry moves. What is missing, and which two ports should the OTLP receiver bind? 5. Name the one field on a Kyverno ClusterPolicy and the one on a Gatekeeper Constraint that decide whether either actually blocks anything. 6. What is the Rego rule head inside a Gatekeeper ConstraintTemplate, and what is the Istio deny-all idiom?

Check your answers
  1. Under spec.syncPolicy.syncOptions as the list item CreateNamespace=true — a sibling of automated, not a child of it. Both prune and selfHeal default to false, so enabling automated sync alone neither deletes removed resources nor reverts manual drift.
  2. declared in the Task under spec.workspaces; declared in the Pipeline under spec.workspaces and mapped per-task (tasks[].workspaces[].workspace); bound to real storage in the PipelineRun (persistentVolumeClaim, volumeClaimTemplate or emptyDir). Miss any one and it fails.
  3. The developer writes the claim — a small namespaced manifest using the kind from the XRD’s claimNames (e.g. PostgreSQLInstance), not the X-prefixed composite kind (XPostgreSQLInstance). The XRD and Composition are platform-team plumbing behind it.
  4. service.pipelines — defining components does nothing until a pipeline wires receivers → processors → exporters together. OTLP ports: 4317 for gRPC and 4318 for HTTP.
  5. Kyverno: spec.validationFailureAction: Enforce (Audit logs only). Gatekeeper: the Constraint’s spec.enforcementAction: deny (dryrun logs only). Then prove it by trying to create a violating pod and seeing the rejection.
  6. The rule head is violation[{"msg": msg}] { … }not deny[msg], which is standalone OPA. The Istio deny-all is an AuthorizationPolicy with action: DENY and rules: [{}] — an empty rule matches everything.