12

Operating Tekton

Writing a Pipeline is the easy part. Running Tekton for a team means starting and debugging runs every day, keeping thousands of finished runs from filling etcd, keeping history somewhere durable, installing and upgrading the components, wiring Git events to PipelineRuns, and noticing when the controller falls behind. This chapter goes through the tools for each job: tkn and the Dashboard, pruning and Tekton Results, the Operator, Pipelines-as-Code, and the controller's metrics, leader election and rate limits.

tkn and the Dashboard: two views of the same objects

tknops-tkn-bundle-pushtekton-dashboard
tkn creates a PipelineRun, then reads it back: list and describe read status from the API server, logs -f follows each step container's log in DAG order, cancel patches spec.status to Cancelled. The Dashboard is a web view over the same objects, so both update together.
sim speed
cluster clock t=0s

terminal · tkn

kubectl context: dev · ns default
# click a command below to run it against the simulated cluster

Tekton Dashboard

PipelineRuns · default
No PipelineRuns yet. Start one from the terminal.
Select a PipelineRun.
PipelineRuns
0
running
0
succeeded
0
failed / cancelled
0

Try: start a run, then follow it with tkn pr logs -f --last and watch the same lines appear in the Dashboard. Tick "a unit test fails", start another, and run tkn pr describe --last: build-image shows up under Skipped Tasks. Start a third and cancel it mid-run.

Retention: runs accumulate in etcd until something deletes them

ops-pruningops-keep-sincetekton-pruner
A PipelineRun with five tasks leaves eleven objects behind: the run, five TaskRuns and five Pods, each with a large status. Nothing removes them by default. The Operator's pruner runs a CronJob that keeps the newest N, or keeps runs younger than some age. The newer tekton-pruner reacts to each completion with a TTL.
pruning

etcd: size of PipelineRuns + TaskRuns + Pods over 30 days

no pruningcurrent (no pruning)
05001k0102030dayMB in etcd
PipelineRuns in cluster
10,799
118,789 objects incl. TaskRuns + Pods
etcd footprint
1,160 MB
peak 1,160 MB · ~110 KB per run
runs deleted
0
of 10,799 created
apiVersion: operator.tekton.dev/v1alpha1
kind: TektonConfig
metadata:
name: config
spec:
pruner:
disabled: true

Try: with no pruning, slide runs/day to 1000 and watch etcd cross its quota. Then pick keep N = 100 on a daily schedule and notice the sawtooth: objects pile up for 24 h between CronJob runs. Switch to hourly or to the event-driven TTL pruner to flatten it.

Pruning deletes history. Once a run is gone, so are its status, results and logs, and the Dashboard can't show it. If you need an audit trail, archive before you delete. That is what Tekton Results is for.

Tekton Results: move history out of the cluster

tekton-resultsresults-watcherops-logs-storage
The Results watcher is a controller that watches PipelineRuns and TaskRuns. When one finishes it sends the full object to the Results API as a Record, stored in PostgreSQL, and uploads its logs. After a grace period it can delete the object from the cluster. You then query history from the API instead of from etcd.
delete after archive

cluster (etcd)

0 PipelineRuns

blue = running · amber = finished, not yet archived · grey = archived, waiting out the grace period

results watcher

Press Play. The watcher is a controller that watches PipelineRuns and TaskRuns.

Results API → PostgreSQL

0 records

logs storage (S3 / GCS / PVC)

query history

$ kubectl get pipelineruns
No resources found in default namespace.
in etcd
0
in Results DB
0
unarchived backlog
0
failed RPCs
0
watcher flags (Deployment args)
- -api_addr=tekton-results-api-service:8080
- -completed_run_grace_period=10s
- -logs_api=true

Try: press Play and watch runs leave the cluster once archived, while the Results API answers for all of them. Then tick "Results API down": finished runs stay in etcd (amber) because the watcher never deletes what it hasn't stored. Bring it back and the backlog drains.

The Tekton Operator: one CR to install them all

tekton-operatortektonconfig-profile
Instead of applying release YAMLs by hand, you write one TektonConfig. Its profile decides which component CRs exist (TektonPipeline, TektonTrigger, TektonChain, TektonDashboard), and each of those is reconciled into Deployments, webhooks and config. It runs continuously, so drift is corrected and upgrades roll through every component.
spec.profile
TektonConfig (you write this)
apiVersion: operator.tekton.dev/v1alpha1
kind: TektonConfig
metadata:
name: config
spec:
profile: basic
targetNamespace: tekton-pipelines
pipeline:
enable-api-fields: beta
pruner:
keep: 100
schedule: "0 8 * * *"

what the operator owns

reconciling…
TektonPipelineto create
tekton-pipelines-controllertekton-pipelines-webhooktekton-events-controllertekton-pipelines-remote-resolvers
TektonTriggerto create
tekton-triggers-controllertekton-triggers-webhooktekton-triggers-core-interceptors
TektonChainto create
tekton-chains-controller
TektonDashboardnot in "basic"
tekton-dashboard
components
3
deployments ready
0/8

operator log

Try: switch lite → all and watch the operator create one component CR at a time, each fanning out to its Deployments. Delete the pipelines controller by hand: the next reconcile puts it back. Press Upgrade and every Deployment rolls to the new release, component by component.

Pipelines-as-Code receives events from a GitHub App (or GitLab, Bitbucket and others), reads .tekton/ at the event's commit, and creates every PipelineRun whose annotations match the event and target branch. It reports progress back as check runs, and PR comments like /retest and /ok-to-test act as commands.
eventsGitOps comments
acme/api
└ .tekton/
.tekton/pull-request.yaml
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: api-pull-request-
annotations:
pipelinesascode.tekton.dev/on-event: "[pull_request]"
pipelinesascode.tekton.dev/on-target-branch: "[main]"
pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
params:
- name: revision
value: "{{ revision }}"
pipelineRef:
name: build

GitHub · checks

Send an event. Pipelines-as-Code reports each PipelineRun as a check run on the commit.

pipelines-as-code controller

PipelineRuns
0
checks passed
0
checks failed
0

Try: open PR #42 and watch the flaky failure land as a red check; comment /retest and only the failed one reruns. Open dana's PR from a fork: nothing runs until a maintainer says /ok-to-test (dana can't approve herself). PR #44 targets release-1.3, so no file matches.

Pipelines-as-Code and Tekton Triggers solve the same problem. Triggers (previous chapter) gives you building blocks: EventListeners, bindings, templates. Pipelines-as-Code is an opinionated product on top: the matching, Git provider auth, status reporting and ACLs are built in, and the PipelineRun lives in the repo.

Observability: metrics, events and logs

tekton-metricsops-k8s-events
The pipelines controller exposes Prometheus metrics on port 9090: a duration histogram per pipeline and status, counts, and gauges of what is running. Each run also gets Kubernetes Events, and the controller writes JSON logs keyed by the object being reconciled.

running_pipelineruns (gauge)

02460204060sim seconds

pipelinerun_duration_seconds (per bucket)

≤ 30s
0
30–60s
0
60–120s
0
120–300s
0
300–600s
0
> 600s
0

Prometheus stores cumulative le buckets; quantiles are interpolated from them, not measured exactly.

what you'd see

# HELP tekton_pipelines_controller_pipelinerun_duration_seconds The pipelinerun execution time in seconds
# TYPE tekton_pipelines_controller_pipelinerun_duration_seconds histogram
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="30"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="60"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="120"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="300"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="600"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="success",le="+Inf"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="30"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="60"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="120"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="300"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="600"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_bucket{namespace="default",pipeline="build",status="failed",le="+Inf"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_sum{namespace="default",pipeline="build"} 0
tekton_pipelines_controller_pipelinerun_duration_seconds_count{namespace="default",pipeline="build"} 0
# TYPE tekton_pipelines_controller_running_pipelineruns gauge
tekton_pipelines_controller_running_pipelineruns 0
running now
0
completed
0
0 failed
p50 duration
—
p95 duration
—
histogram_quantile(0.95, …_bucket)

Try: Play for a while, then tick "image registry is slow". The running gauge climbs first (Little's law: same arrivals, longer stays), then completions shift into the 300 s+ buckets and p95 turns red. That is the alert you want, before users notice.

Scaling the controller: leader election buckets and API rate limits

ops-leader-electionops-controller-qps
The controller can run as several replicas. The reconcile key space is split into buckets, each guarded by a Lease, and a replica only processes keys whose bucket lease it holds. Separately, every replica throttles its own calls to the API server with a client-side token bucket (QPS and burst), and runs a fixed number of worker threads.
t=0s
replica-0leader
buckets 0
replica-1standby
—
replica-2standby
—

leases (one per bucket)

bucket 0r-0
renewing

reconcile keys → bucket = hash(key) mod buckets

0·b017wdr·b0grnhi·b01oo19·b0xjaz0·b06ekos·b01eayg·b0n686a·b01v2ly·b013xvn·b0ct5dk·b01kpj5·b0tksv2·b02g2ku·b01acgc·b0j7q2c·b01r43u·b0zzdju·b08un9m·b01gr11·b0pmar4·b01xioi·b016dy8·b0f97ye·b01n5lq·b0w0vfw·b04w55o·b01csix·b0lnsn6·b01tk6e·b0
keys stalled now
0/30
active replicas
1
ones holding at least one lease
worst failover gap
0s
≈ lease-duration + retry
key-seconds stalled
0
TektonConfig spec.pipeline.performance
performance:
disable-ha: false
replicas: 3
buckets: 1
threads-per-controller: 2
# config-leader-election
# lease-duration: 60s
# retry-period: 10s

Try: with 1 bucket and 3 replicas only one pod works; kill it and every key stalls for about a minute until a standby's lease attempt succeeds. Set buckets to 6: work spreads across all replicas, and killing one stalls only its share of keys.

PipelineRuns fully reconciled over time

defaults (2 threads, qps 5, burst 10)your settings
01002003000m3m7m10m13mseconds after the spikeruns reconciled300 runs
time to drain
12.0 min
defaults: 12.0 min
reconciles / s
2.5
0.42 runs/s steady
bottleneck
API rate limit
raise kube-api-qps/burst
TektonConfig spec.pipeline.performance
performance:
threads-per-controller: 2
kube-api-qps: 5
kube-api-burst: 10

Try: at the defaults, 300 runs take minutes because the client-side rate limiter (qps 5) caps API calls; adding threads does nothing. Raise kube-api-qps to 50 and the bottleneck moves to threads; raise those too. Burst only helps the first few seconds. (Rough model: each run needs ~6 reconciles of ~2 API calls each.)

  • takeawaytkn, the Dashboard and kubectl all read the same objects. Runs live in etcd, so they need a retention policy (prune) and, if you care about history, an archive (Results) before deletion.
  • takeawayLet controllers do the operating: the Operator reconciles installs and upgrades from one TektonConfig, and Pipelines-as-Code turns Git events and PR comments into PipelineRuns and check runs.
  • takeawayWatch duration histograms and running gauges. When load grows, the API rate limiter is usually the first bottleneck; buckets let several replicas share the work and shrink the impact of a crash.

Key concepts · 20

book chapter 12
  1. tkn CLI

    The Tekton command-line client: start, list, describe, follow logs of, cancel and delete Tekton objects.

    It is a convenience layer over the Kubernetes API. Everything it does is reading or writing the same CRDs kubectl sees, plus following Pod container logs in DAG order.

    tkn pipeline starttkn pr logs -f --lasttkn tr describetkn pr canceltkn pr delete --keep 5
    ↑ see it in the visualization
  2. tkn bundle push

    Packages Tasks and Pipelines into an OCI image (a Tekton Bundle) and pushes it to a registry.

    Bundles version and distribute pipeline definitions like container images; the bundles resolver fetches them by digest at run time.

    tkn bundle pushtkn bundle listresolver: bundles
    ↑ see it in the visualization
  3. Tekton Dashboard

    A web UI that lists PipelineRuns and TaskRuns and shows their tasks, steps and logs.

    It reads the same live objects as tkn. When an object is pruned it disappears from the Dashboard too, unless logs and records are served from an external store like Results.

    tekton-dashboard--read-only--external-logs
    ↑ see it in the visualization
  4. Pruning

    Deleting finished PipelineRuns and TaskRuns (and with them their Pods) so they don't accumulate in etcd.

    Every run leaves several status-heavy objects behind. Without cleanup, etcd grows until it hits its quota and the API server starts refusing writes, which breaks far more than CI.

    TektonConfig spec.prunertkn pr delete --keepetcd --quota-backend-bytes
    ↑ see it in the visualization
  5. keep / keep-since

    The operator pruner's two retention rules: keep the newest N runs, or keep runs younger than M minutes. They are mutually exclusive.

    The operator implements them as a CronJob, so objects still pile up between schedule firings. Keep N bounds the count; keep-since bounds age but lets busy days grow large.

    spec.pruner.keepspec.pruner.keep-sincespec.pruner.schedulespec.pruner.resources
    ↑ see it in the visualization
  6. tekton-pruner

    A newer, event-driven pruner controller that deletes runs based on TTL after completion and per-status history limits.

    Reacting to completions instead of a schedule removes the CronJob sawtooth and allows global, namespace or per-pipeline policies.

    ttlSecondsAfterFinishedsuccessfulHistoryLimitfailedHistoryLimittekton-pruner-default-spec
    ↑ see it in the visualization
  7. Tekton Results

    A service that stores finished PipelineRuns and TaskRuns as Results and Records in a database, with an API to query them.

    It separates history from live cluster state: once a run is archived it can be deleted from etcd and still be queried, audited and shown in UIs.

    results.tekton.dev/v1alpha2parents/{ns}/results/{uid}/recordsPostgreSQLtkn-results
    ↑ see it in the visualization
  8. Results watcher

    The Results controller that watches runs in the cluster and sends each one to the Results API over gRPC, plus its logs.

    It only deletes an object after it was stored and a grace period passed, so an outage of the API makes objects linger instead of losing history.

    tekton-results-watcher-completed_run_grace_periodresults.tekton.dev/record annotation
    ↑ see it in the visualization
  9. Logs storage

    Where Results keeps step logs after the Pods are gone: object storage like S3 or GCS, or a PVC.

    Pod logs vanish with the Pod. Archiving them is what keeps a failed build debuggable a week later.

    LOGS_APILOGS_TYPE=S3|GCS|File
    ↑ see it in the visualization
  10. Tekton Operator

    A Kubernetes operator that installs, configures and upgrades Tekton components from a single TektonConfig resource.

    It reconciles continuously: delete a controller by hand and it comes back; bump the operator and every component rolls forward together.

    TektonConfigTektonPipelineTektonTriggerTektonChainTektonDashboardTektonInstallerSet
    ↑ see it in the visualization
  11. TektonConfig profiles

    spec.profile picks which components the operator installs: lite (Pipelines), basic (adds Triggers and Chains) or all (adds the Dashboard).

    One field decides the footprint. Changing it adds or removes component CRs, and the operator handles the rest.

    profile: liteprofile: basicprofile: alltargetNamespace
    ↑ see it in the visualization
  12. Pipelines-as-Code

    A Tekton project that runs PipelineRuns kept in the repository's .tekton/ directory in response to Git provider events.

    Pipeline definitions are versioned and reviewed with the code they build, and a PR can change its own CI.

    Repository CRGitHub Apptkn pac{{ revision }}
    ↑ see it in the visualization
  13. .tekton directory

    The folder Pipelines-as-Code scans on each event for PipelineRun YAML files to match.

    Each file is one candidate PipelineRun; annotations on it decide whether it runs for this event.

    .tekton/pull-request.yaml.tekton/push.yaml
    ↑ see it in the visualization
  14. on-event / on-target-branch

    Annotations that match a PipelineRun to an event type and target branch (globs allowed); on-cel-expression allows richer matching.

    Matching happens before anything is created, so unmatched events cost nothing and you can keep separate runs for PRs, merges and tags.

    pipelinesascode.tekton.dev/on-eventpipelinesascode.tekton.dev/on-target-branchon-cel-expressionmax-keep-runs
    ↑ see it in the visualization
  15. GitOps commands

    PR comments that drive CI: /retest, /test <name>, /cancel, and /ok-to-test to approve a run for an untrusted author.

    Running a fork's PR means running a stranger's code with your credentials, so it waits for a maintainer; comments from users outside the ACL are ignored.

    /retest/ok-to-test/test <pipelinerun>/cancelOWNERS
    ↑ see it in the visualization
  16. Status as GitHub Checks

    Pipelines-as-Code reports each PipelineRun back to the commit as a check run (or commit status), with a summary of its tasks.

    Developers see CI where they already are, and branch protection rules can require the checks to pass before merge.

    Check Runs APIcommit status
    ↑ see it in the visualization
  17. Controller metrics

    Prometheus metrics exposed by the controller, such as pipelinerun_duration_seconds (histogram) and running_pipelineruns (gauge), all prefixed tekton_pipelines_controller_.

    Duration percentiles and running counts are the first signal of a slow registry, an overloaded cluster or a stuck controller. config-observability controls levels and histogram vs last-value.

    …_pipelinerun_duration_seconds…_running_pipelinerunsconfig-observabilityhistogram_quantile
    ↑ see it in the visualization
  18. Events and controller logs

    Kubernetes Events the controller emits on runs (Started, Running, Succeeded, Failed) and its structured JSON logs keyed by namespace/name.

    Events explain one run's story; logs explain what the controller decided and why. Both are short-lived, so ship them somewhere if you need them later.

    kubectl get eventsknative.dev/keyconfig-logging
    ↑ see it in the visualization
  19. HA leader election (buckets)

    Running several controller replicas that split the key space into buckets, each guarded by a Lease that one replica holds at a time.

    With one bucket only one replica works and the rest wait. More buckets spread the load, and a crash stalls only the dead replica's share until its leases expire.

    config-leader-electionbucketslease-durationcoordination.k8s.io/Lease
    ↑ see it in the visualization
  20. Controller QPS, burst and threads

    Performance knobs: kube-api-qps and kube-api-burst limit the controller's client-side API rate; threads-per-controller sets workqueue workers.

    Defaults are conservative. Under a spike of runs the rate limiter, not CPU, is usually the bottleneck, so raise QPS and burst before adding threads.

    -kube-api-qps-kube-api-burst-threads-per-controllerspec.pipeline.performance
    ↑ see it in the visualization