01

Kubernetes-native CI/CD

Most CI systems are servers: a controller holds the job definitions and hands work to a pool of agents. Tekton has no such server. It adds new resource types to Kubernetes (Task, TaskRun, Pipeline, PipelineRun) and a controller that turns those objects into Pods. Your pipeline is YAML stored in etcd, validated by an admission webhook, run by the kubelet, and cleaned up by the garbage collector. This chapter takes that machinery apart: the objects, the reconcile loop, the webhook, the scheduling model, the wider project, and the flags that decide which fields a cluster accepts.
Applying a Task stores an object; nothing runs. Creating a PipelineRun is what starts work: the controller creates a TaskRun for each pipeline task and a Pod for each TaskRun, stamping an ownerReference on every child. Deleting the run lets Kubernetes' garbage collector walk those links.

etcd · objects and their ownerReferences

idle
definitionsPipelineRunsTaskRunsPodsetcd is empty. Apply some YAML.
──▸ ownerReference (child → owner, drives garbage collection)┄┄ taskRef / pipelineRef (by name, no ownership)
Runs
0
TaskRuns
0
Pods
0
kubectl + controller log
# a kind cluster with Tekton Pipelines installed
kubectl get
$ kubectl get tekton,pods
No resources found in default namespace.

Try: create a PipelineRun before applying anything (it fails with CouldntGetPipeline). Then apply both files, run twice, and delete a run with each cascade mode. Deleting the Task does nothing to the runs: a reference by name is not ownership.

The controller never runs your build. It watches TaskRuns and Pods, puts the key of anything that changed on a workqueue, and for each key compares what the spec asks for with what exists: no Pod yet means create one; a Pod that moved on means update status. Step through it event by event, then crash the controller in the middle.

cluster (actual state)

TaskRun · desired state (spec)
build-run
clonebuildpush
Pod · actual state
none

The scheduler and kubelet move the Pod forward on their own. Each change fires an informer event at the controller, if it is listening.

controller

● running
workqueue
empty
reconciler log
# controller watching TaskRuns and Pods
status vs Pod
in sync
reconciles
0
events missed
0
deduplicated
0
same key, one reconcile
kubectl get taskrun build-run -o yaml
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: {name: build-run}
spec:
taskRef: {name: build} # steps: clone, build, push
status:
{} # not reconciled yet

Try: play, and crash the controller while the Pod is running. Fire a couple of cluster events while it is down, then restart. In level-triggered mode one resync reads the current Pod and catches up. Switch to edge-triggered and do the same: the missed "step finished" events never arrive, so the count never reaches 3 and the TaskRun stays Running forever.

The reason a TaskRun can say Succeeded=Unknown is that the condition is a summary computed from the Pod, not a log of what happened. The detailed lifecycle (Pending, Running, retries, timeouts) is in chapter 03.

Admission: rejected before it is stored

admission-webhookconfig-defaultstekton-pipelines-namespace
Every create or update passes through the API server's admission chain. Tekton registers a defaulting (mutating) webhook and a validating webhook, both served by tekton-pipelines-webhook. Typos, missing images and gated fields fail at kubectl apply, with a field path, instead of as a broken Pod.
enable-api-fields

the request path through the API server

kubectlclientauthn / authzRBACdefaultingwebhook.pipeline…validationvalidation.webhook…etcdstoredtekton-pipelines-webhook (namespace tekton-pipelines)
taskrun.yaml
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: build-run
spec:
params:
- name: revision
value: main
taskSpec:
params:
- name: revision
steps:
- name: clone
image: alpine/git
script: git checkout $(params.revison)
- name: build
image: golang:1.23
script: go build ./...
terminal
$ kubectl apply -f taskrun.yaml
# press send

Try: send it as is (the param typo is caught before anything runs). Fix it and send again to see the defaulting webhook add three fields. Then tick stdoutConfig and flip enable-api-fields to alpha.

Agents vs Pods

pod-per-taskrun
A traditional CI server queues jobs for a fixed set of agents, each one machine running one job. In Tekton each TaskRun is a Pod with a resource request; the scheduler packs Pods onto whatever nodes exist, and a cluster autoscaler can add or remove nodes. Below, the same seeded stream of builds over a working day, with two bursts, runs both ways.

traditional CI server · fixed agents

2 CPU per agent
controllerqueue
agent-1#53
agent-2#55
agent-3#51
agent-4#56
agent-5#49
agent-6#52

Agents are long-lived machines with tools preinstalled. They cost the same whether busy or idle, and a burst queues behind them.

Tekton · every TaskRun is a Pod

4 CPU per node
kube-schedulerpending
none
node-1
node-2
node-3
node-4
node-5

No build agents: each run is a Pod packed onto nodes by CPU request. With the autoscaler, nodes appear when Pods are pending and go away when empty (max 10).

queue length over the day · 10:30

CI queuepending Podsnodes
0510152008:0011:2014:4018:00
CI wait p95
29m
avg 10.9m
Pod wait p95
3m
avg 0.4m
CI CPU-hours
132
39% used
cluster CPU-hours
98
53% used
builds
233
peak nodes
6

Try: at 24 builds/hour, size the CI pool so its p95 wait is under 10 minutes and compare CPU-hours with the autoscaled cluster. Then turn the autoscaler off: Tekton is not magic, a fixed cluster queues Pods the same way. What changes is that capacity is shared and elastic, not tied to a pet build server.

The Tekton project, mapped

tekton-componentscdf
Tekton Pipelines is the core; the other projects plug into it through the same Kubernetes API. Triggers and Pipelines-as-Code start runs, resolvers pull shared Tasks, Chains signs results, Results archives them, Dashboard and tkn show them, and the Operator installs the lot. Tekton is hosted by the Continuous Delivery Foundation.
Click any component to see where it sits.

the Tekton project, as one flow

Git providerGitHub · GitLabTriggersEventListenerPipelines-as-Code.tekton/ folderPipelinescontrollersKubernetesscheduler · kubeletHub / Catalogvia resolversChainssign · attestResultslong-term historyDashboardweb UItknCLIOCI registryimages · signaturesOperatorTektonConfig

Pipelines

The core. It adds the CRDs and runs the controller that reconciles PipelineRuns into TaskRuns and TaskRuns into Pods, plus the admission webhook that defaults and validates them.

adds
Task, TaskRun, Pipeline, PipelineRun, StepAction, CustomRun
connections
  • ← Triggers: creates PipelineRun
  • ← Pipelines-as-Code: creates PipelineRun
  • ← Hub / Catalog: resolved Task
  • → Kubernetes: creates Pods
  • → Chains: TaskRun done
  • → Results: run records
  • ← Dashboard: reads / creates
  • ← tkn: kubectl-style API
github.com/tektoncd/pipeline
→ chapter 02–07 · the building blocks

Try: follow a git push, then click the Operator. Every Tekton component is just more CRDs and controllers on the same API server; none of them is a central build server.

Declarative, versioned, portable

tekton-v1-apifeature-flagsenable-api-fields
The same v1 YAML runs on any conformant cluster with Tekton installed, as long as it only uses fields that cluster enables. Each installation's feature-flags ConfigMap sets enable-api-fields to stable, beta (the default) or alpha, and the validating webhook enforces it.

the same YAML, applied to three clusters

click a gate to change it
laptop (kind)
✓ pipeline.tekton.dev/ci created
staging (EKS)
✓ pipeline.tekton.dev/ci created
prod (GKE)
✗ denied: matrix fan-out
pipeline.yaml on prod (GKE)
apiVersion: tekton.dev/v1
kind: Pipeline # fragments, one per feature
# when expressions · stable
when:
- input: $(params.branch)
operator: in
values: [main]
# StepAction ref · stable
steps:
- name: clone
ref: {name: git-clone}
# matrix fan-out · beta
matrix:
params:
- name: goos
value: [linux, darwin]
kubectl get cm feature-flags -n tekton-pipelines -o yaml # prod (GKE)
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flags
namespace: tekton-pipelines
data:
enable-api-fields: "stable"
await-sidecar-readiness: "true"
coschedule: "workspaces"
results-from: "termination-message"
The validating webhook on prod (GKE) rejects matrix fan-out: those fields need enable-api-fields at beta. v1 is the storage version; v1beta1 objects are still served and converted by the webhook.

Try: keep only stable features and every cluster accepts the file. Add matrix or CEL and watch prod reject it. Portability holds as long as you stay within the level every target cluster enables.

  • takeawayTekton is CRDs plus controllers. Definitions and runs are ordinary API objects, so RBAC, namespaces, kubectl, GitOps and garbage collection apply to CI/CD unchanged.
  • takeawayThe controller is level-triggered: it reconciles from the current state, not from a history of events, which is why a restart or a burst of missed updates heals itself.
  • takeawayThere are no build agents. Each TaskRun is a Pod scheduled by Kubernetes, so capacity is the cluster's and portability is the YAML's, limited only by which feature gates each cluster enables.

Key concepts · 19

book chapter 1
  1. CustomResourceDefinition (CRD)

    A Kubernetes object that adds a new resource type, such as Task or PipelineRun, to the API server.

    Because Tekton's types are CRDs, kubectl, RBAC, namespaces, labels, watches and GitOps tools all work on pipelines exactly as they do on Deployments. There is no separate Tekton API or database.

    kubectl get crd | grep tekton.devkubectl explain task.spec.steps
    ↑ see it in the visualization
  2. Custom resource

    An instance of a CRD: one Task, one PipelineRun, stored in etcd like any other object.

    Definitions (Task, Pipeline) and executions (TaskRun, PipelineRun) are separate objects. You keep the first in Git and create the second each time something should run.

    task.tekton.dev/git-clonekubectl get pipelineruns
    ↑ see it in the visualization
  3. Task and TaskRun

    A Task is a reusable list of steps; a TaskRun is one execution of it, with concrete params and workspaces.

    Splitting what from when means a Task can be shared, versioned and reviewed, while each TaskRun records exactly what ran and how it ended.

    spec.taskRef.nametkn task start
    ↑ see it in the visualization
  4. Pipeline and PipelineRun

    A Pipeline arranges Tasks into a graph; a PipelineRun executes it by creating one TaskRun per pipeline task.

    The PipelineRun owns its TaskRuns, which own their Pods, so one object represents the whole run and deleting it cleans up everything beneath.

    spec.pipelineRef.nametkn pipeline start
    ↑ see it in the visualization
  5. ownerReference

    metadata.ownerReferences on a child object pointing at the object that created it: Pod → TaskRun → PipelineRun.

    Kubernetes' garbage collector follows these links. A taskRef by name is not ownership: deleting a Task leaves its past runs alone.

    metadata.ownerReferencesblockOwnerDeletion
    ↑ see it in the visualization
  6. Cascading deletion

    When an owner is deleted the garbage collector deletes its dependents, in background (default), foreground, or not at all (orphan).

    Pruning old PipelineRuns is enough to reclaim their TaskRuns and Pods. Run history therefore lives in etcd only until you prune it, which is why Tekton Results exists.

    kubectl delete pr --cascade=foregroundtkn pipelinerun delete --keep 5
    ↑ see it in the visualization
  7. Controller

    A long-running process that watches resources through informers and works to make the actual state match the declared one.

    Tekton's tekton-pipelines-controller is the only Tekton process on the run path. It does not execute builds; it creates Pods and lets the kubelet run them.

    deployment/tekton-pipelines-controllerknative.dev/pkg
    ↑ see it in the visualization
  8. Reconcile loop

    The function reconcile(key) called for each queued object: read the spec and the current world, act, update status.

    Changes are coalesced into a workqueue of keys, so ten Pod updates in a burst can mean a single reconcile. The reconciler never assumes it saw every event.

    workqueueinformer resync
    ↑ see it in the visualization
  9. Level-triggered

    Acting on the current state (the level) rather than on the sequence of changes (the edges).

    If the controller crashes or misses events, a fresh LIST and one reconcile per object are enough to catch up. Edge-triggered logic that counts events stays wrong forever after a miss.

    ↑ see it in the visualization
  10. Status conditions

    status.conditions on every run; Tekton's key condition is type Succeeded with status Unknown, True or False and a reason.

    Unknown means still going (reason Pending, Running…); True and False are final (Succeeded, Failed, TaskRunTimeout, Cancelled…). Every tool, from tkn to Dashboard, reads this one field.

    status.conditions[type=Succeeded]kubectl wait --for=condition=Succeeded
    ↑ see it in the visualization
  11. Admission webhook

    An HTTPS hook the API server calls before storing an object: mutating (defaulting) first, then validating.

    Tekton's webhook rejects bad YAML (missing image, unknown variable, gated field) when you apply it, not minutes later when a Pod fails. It also fills defaults such as timeout and serviceAccountName.

    validation.webhook.pipeline.tekton.devwebhook.pipeline.tekton.devconfig-defaults
    ↑ see it in the visualization
  12. config-defaults

    A ConfigMap in tekton-pipelines holding cluster-wide defaults applied by the defaulting webhook.

    default-timeout-minutes (60) and default-service-account (default) are why a TaskRun you never gave a timeout still has one.

    default-timeout-minutesdefault-service-accountdefault-pod-template
    ↑ see it in the visualization
  13. feature-flags ConfigMap

    The ConfigMap that switches Tekton behaviours and API fields on or off for the whole installation.

    It controls stability gates and runtime choices such as await-sidecar-readiness, results-from and coschedule. Two clusters with different flags can accept different YAML.

    kubectl edit cm feature-flags -n tekton-pipelines
    ↑ see it in the visualization
  14. enable-api-fields

    The flag that sets which API fields the webhook accepts: stable, beta (the shipped default) or alpha.

    Alpha fields may change or disappear; beta fields follow a gentler policy; stable fields are covered by the v1 compatibility promise. Gating them per cluster keeps production on a known surface.

    enable-api-fields: betaenable-param-enumenable-cel-in-whenexpression
    ↑ see it in the visualization
  15. tekton.dev/v1

    The stable API version for Task, TaskRun, Pipeline and PipelineRun; v1beta1 is still served and converted by the webhook.

    v1 dropped PipelineResources and renamed some fields (a step's resources became computeResources). Writing v1 YAML is what makes it portable across conformant clusters.

    apiVersion: tekton.dev/v1conversion webhook
    ↑ see it in the visualization
  16. tekton-pipelines namespace

    Where the Pipelines controller, webhook, resolvers and their ConfigMaps run.

    Tekton is installed with one kubectl apply of release.yaml (or by the Operator); nothing runs outside the cluster.

    kubectl get pods -n tekton-pipelinestekton-pipelines-resolvers
    ↑ see it in the visualization
  17. A Pod per TaskRun

    Every TaskRun executes as one ordinary Pod that the Kubernetes scheduler places on a node.

    There are no long-lived build agents to size, patch or keep warm: capacity is the cluster's, it bin-packs by resource request, and it scales with the node pool.

    ↑ see it in the visualization
  18. Tekton components

    Pipelines (core), Triggers, Chains, Results, Dashboard, CLI (tkn), Operator, the catalog/Hub, and Pipelines-as-Code.

    Each one is more CRDs and controllers on the same API server. You can adopt Pipelines alone and add the others as you need them.

    tektoncd/pipelinetektoncd/triggerstektoncd/chainstektoncd/operator
    ↑ see it in the visualization
  19. Continuous Delivery Foundation

    The Linux Foundation project that hosts Tekton, alongside Jenkins, Spinnaker and others.

    Vendor-neutral governance is why Tekton serves as the engine under several products (OpenShift Pipelines and Jenkins X among them) and why its API is meant as a common standard.

    cd.foundation
    ↑ see it in the visualization