Tekton's whole trick is that a pipeline is not config for a CI server; it is Kubernetes resources executed as pods. Once that lands, everything else is vocabulary, and debugging CI becomes debugging pods, which you already know how to do.
make core cicdOrientation
A Task is a sequence of steps (containers sharing one pod). A Pipeline sequences Tasks. Running either means creating a TaskRun or a PipelineRun. That is the entire object model, and every failure you will ever debug is a container in a pod that exited non-zero.
Pipeline ──instantiated by──▶ PipelineRun
└─ tasks[] └─ creates one TaskRun per task
└─ Task └─ creates one pod
└─ steps[] └─ one container per step, run in order
sharing /workspace volumes
Steps run sequentially in the same pod, so they share volumes and the node; that is how the clone step's checkout reaches the build step. It also means a step's resource requests are not additive across steps (the pod gets the max), that a step cannot run on a different node, and that everything in a Task lives or dies together. Tasks, in contrast, are separate pods and may run in parallel.
The pieces the exam wires together
| Piece | Carries | Syntax you will type |
|---|---|---|
| params | values down: pipeline → task → step | $(params.image) |
| workspaces | files between tasks | $(workspaces.source.path) |
| results | small strings between tasks | $(tasks.clone.results.commit) |
| when | conditional execution | when: [{input: "$(params.env)", operator: in, values: ["prod"]}] |
| finally | tasks that always run (cleanup, notify) | spec.finally[] |
| matrix | fan-out one task over a list | matrix.params |
Params have types (string, array, object) and defaults. Results are written by a step to $(results.<name>.path) and are deliberately small: they travel through the TaskRun's status, so a result is a digest or a count, never a build log. Workspaces are bound at run time by the PipelineRun: an emptyDir (per-pod, lost between tasks), a persistentVolumeClaim (shared, you manage it), or a volumeClaimTemplate (shared, created and deleted with the run; the sane default). ConfigMaps and Secrets can also back a workspace, which is the tidy way to hand credentials to a step.
Tasks with no runAfter and no shared results run in parallel. Consuming another task's result creates an implicit dependency, so removing a result reference can silently parallelise a pipeline that used to be sequential. If order matters, say it with runAfter rather than relying on a data dependency you might refactor away.
Identity, credentials, and registries
The pipeline pod runs as a ServiceAccount (taskRunTemplate.serviceAccountName on the PipelineRun), and in real setups that SA carries registry credentials as an imagePullSecret or a kubernetes.io/dockerconfigjson Secret annotated for Tekton. The lab's registry needs none, and kaniko pushes to it with --insecure.
The pipeline pushes to kind-registry:5000/demo:v1 because it runs inside the cluster, where localhost is the pod itself. From the host, the very same store answers as localhost:5001. Knowing which name works from where is a real exam skill, and it comes back in section 5.6 when Kyverno has to fetch a signature from inside the cluster.
Where Tasks come from
The catalog tasks make cicd installed are git-clone and kaniko; the trivy-scan task arrives when you apply the lab example. tkn task list shows what you have. Modern Tekton can also fetch task definitions at run time with resolvers: git, hub, or bundles (OCI images containing task YAML), via taskRef.resolver. That is how a platform team ships a shared, versioned task library without copying YAML into every repo, and it is worth being able to describe even if you never configure one.
Triggers: from a git push to a run
Three CRDs to say "when Gitea posts a push event, run the pipeline with that commit":
- EventListener: a pod exposing HTTP that receives the webhook.
- TriggerBinding: extracts fields from the payload (
$(body.repository.clone_url),$(body.after)). - TriggerTemplate: stamps out the PipelineRun with those values.
- Plus interceptors, the part people forget: filter by event type, verify the webhook secret, or evaluate a CEL expression before anything runs. Without one, your EventListener happily builds anything anyone posts to it.
Debugging, which is just pod debugging
| Symptom | Most likely | Command |
|---|---|---|
| Run stays Pending | workspace PVC unbound, or quota | kubectl describe pod -l tekton.dev/pipelineRun=<name> |
| Step fails immediately | image pull, bad command, missing param | tkn pipelinerun logs <name> -f |
| "couldn't find task" | wrong namespace, or a resolver misconfigured | tkn task list |
| Permission denied pushing | SA has no registry secret | kubectl get sa <sa> -o yaml |
| Everything ran in parallel | missing runAfter | read the Pipeline, not the logs |
| Run Succeeded, nothing shipped | a step swallowed a non-zero exit | tkn taskrun describe --last |
Every step is a container named step-<name> in the TaskRun's pod, so kubectl logs <pod> -c step-build works when tkn is being unhelpful. The condition on the run carries the summary message, and kubectl get taskrun -o jsonpath='{.items[0].status.conditions[0].message}' is the scriptable form the exam's grader would use.
Both run DAGs of containers, and the exam lists both. The distinction to state: Tekton is CI-shaped: build, test, publish artifacts, triggered by git events, with a task catalog built around that. Argo Workflows (section 3.4) is general orchestration: provisioning sequences, scheduled batch jobs, anything run-to-completion. Overlapping capability, different centre of gravity, and section 3.6 makes you defend the choice.
Exercises
Read examples/tekton/pipeline.yaml first: pipeline build-and-scan, params repo-url and image, one workspace shared carried clone → build → scan, and a ready-made PipelineRun with generateName. One catch the file does not solve for you: the seeded demo-app repo contains only a README, and kaniko needs a Dockerfile. Supplying one is the exercise:
source lab.env # push needs credentials
git clone "http://lab:${GITEA_PASS}@gitea.lab:3000/lab/demo-app.git" /tmp/demo-app && cd /tmp/demo-app
cat > Dockerfile <<'EOF'
FROM ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine
COPY README.md /usr/share/nginx/html/index.html
EOF
git add . && git commit -m "make it buildable" && git push
cd - && kubectl create -f examples/tekton/pipeline.yaml # Task + Pipeline + one PipelineRun
tkn pipelinerun logs --last -fIf the scan step fails instead, that is the CVE gate doing its job on today's base image; read which CVEs and decide as a platform engineer would (bump the base, or ignore-unfixed). Rerun later with tkn pipeline start build-and-scan --last.
skopeo inspect --tls-verify=false docker://localhost:5001/demo:v1 | jq .Digest.Write a Task manifest-lint that takes a param path, mounts workspace source, and runs kustomize build $(params.path) from an image that has kustomize (registry.k8s.io/kustomize/kustomize:v5.0.0 works, or line-count with busybox if pulls are slow). Emit a result objects containing the object count. Run it with tkn task start against the platform repo cloned by a git-clone task, or standalone with a fresh clone step.
tkn taskrun describe --last shows your result value, non-zero.The trivy-scan task exits 1 on HIGH/CRITICAL findings, and an old image guarantees some:
tkn task start trivy-scan -p image=nginx:1.19 -w name=source,emptyDir="" --showlog
tkn taskrun list | head -3
kubectl get taskrun -o jsonpath='{.items[0].status.conditions[0].message}'Then reason one level up: in the full pipeline, a scan failure stops anything sequenced after it via runAfter, which is the entire supply-chain argument for putting the gate in the pipeline instead of in a ticket.
kubectl output alone, without the dashboard, is the competency.EventListener + TriggerBinding (extract the clone URL and SHA from Gitea's push payload) + TriggerTemplate (PipelineRun with those as params). Expose the EventListener service as a LoadBalancer so the Gitea container can reach it across the kind network, then add the webhook in the Gitea UI (repo settings → webhooks, target http://<EXTERNAL-IP>:8080). Push a commit to demo-app. Budget an hour; the payoff is having debugged webhook → listener → run once before an exam asks you to.
tkn pipelinerun list grows by one without you touching kubectl, and kubectl get eventlistener shows Ready.Self-check
Two tasks in a pipeline run at the same time and you did not expect it. Why?
No runAfter and no result dependency between them, so Tekton runs them concurrently by design. Order comes from explicit runAfter or from consuming a result; nothing else implies sequence.
How do files get from the clone task to the build task, and what happens if you bind the workspace to an emptyDir?
Through a workspace backed by a PVC or volumeClaimTemplate, mounted by both tasks. An emptyDir is per-pod, so each task gets an empty one and the build finds nothing: a classic "why is my workspace empty" bug.
Where do you put a step that must run whether the pipeline passed or failed?
spec.finally. It runs after all other tasks regardless of outcome, and it can inspect $(tasks.status) to branch; cleanup, notifications, and teardown belong there rather than as a last runAfter task that never executes on failure.
Why does the pipeline push to kind-registry:5000 and not localhost:5001?
Because it runs in a pod, where localhost is the pod's own network namespace. The registry has an in-cluster DNS name wired into CoreDNS and containerd; the host reaches the same store through a published port. Same content, two names, and the digest is identical from both.
Your EventListener is Ready and a push produces nothing. Diagnostic ladder?
Did the webhook deliver (Gitea's webhook delivery log)? Did the listener receive it (its pod logs)? Did an interceptor filter it out (event type, secret mismatch, CEL expression)? Did the binding extract fields the template expects (a missing param yields an invalid PipelineRun)? Each rung produces a different error, and skipping one is how people lose ten minutes.
Docs to know your way around
- tekton.dev: Tasks, Pipelines, and the workspaces page (volumeClaimTemplate binding especially); Triggers' TriggerBinding examples for payload paths; the resolvers page.
- Offline:
kubectl explain pipelinerun.spec,tkn <verb> --help, andtkn task describe <name>to read a catalog task's params and workspaces without leaving the terminal.