Tekton's pitch is simple to state and easy to underestimate: your CI/CD pipeline is not a YAML file interpreted by some external service, it's a set of Kubernetes custom resources, reconciled by controllers running in your own cluster. A build is a PipelineRun object. You can kubectl get it, watch it, delete it, and write a controller against it if you want to. That single design decision — CI as Kubernetes-native objects instead of CI as an opaque product bolted onto Kubernetes — is what makes Tekton interesting, and it's also the entire source of its operational cost. This is what running it has actually looked like.
Tasks, Pipelines, and PipelineRuns
Tekton has three core building blocks, and the naming is unusually literal. A Task is a sequence of steps, where each step runs in its own container — think of it as one job's worth of work: clone a repo, run a build, push an image. A Pipeline wires multiple Tasks together with an explicit ordering (via runAfter or shared results). Neither a Task nor a Pipeline does anything by itself — they're templates. The thing that actually executes is a TaskRun or a PipelineRun, which is what you create (directly, or via a trigger) to kick off real work. Every step in a Task runs as a container in the same pod, which means steps share a filesystem by default and you don't need a separate volume-mount dance to pass a compiled binary from a build step to a push step.
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: build-and-push
spec:
params:
- name: image
type: string
workspaces:
- name: source
steps:
- name: build
image: gcr.io/kaniko-project/executor:latest
workingDir: $(workspaces.source.path)
args:
- --dockerfile=Dockerfile
- --context=$(workspaces.source.path)
- --destination=$(params.image)
- --cache=true
A Pipeline strings Tasks together and declares the workspace(s) that get passed between them:
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: ci-pipeline
spec:
workspaces:
- name: shared-source
params:
- name: image
type: string
tasks:
- name: fetch
taskRef:
name: git-clone
workspaces:
- name: output
workspace: shared-source
- name: build
runAfter: ["fetch"]
taskRef:
name: build-and-push
params:
- name: image
value: $(params.image)
workspaces:
- name: source
workspace: shared-source
And a PipelineRun is the concrete execution — this is the object your CI trigger actually creates, and the one you'll kubectl describe at 11pm when a build is stuck:
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: ci-pipeline-run-a3f9c1
spec:
pipelineRef:
name: ci-pipeline
params:
- name: image
value: registry.internal/app:a3f9c1
workspaces:
- name: shared-source
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
How this compares operationally to Jenkins and GitHub Actions
Jenkins runs as a stateful application with a plugin ecosystem you have to patch, and its notion of a build agent is bespoke to Jenkins — a Kubernetes plugin can spin up pod-based agents, but the orchestration logic lives inside Jenkins' own scheduler, not in Kubernetes' scheduler. Tekton flips that: every run is a pod, scheduled by the kube-scheduler like anything else in the cluster, subject to the same resource quotas, node affinity, and RBAC as your application workloads. That's genuinely nice when you already have Kubernetes expertise in-house — you debug a stuck pipeline the same way you debug a stuck deployment, with kubectl describe pod and kubectl logs, not a Jenkins-specific UI with its own quirks.
GitHub Actions (or GitLab CI, or Buildkite) is the opposite trade: it's a hosted control plane you don't operate, with a job-scheduling and secrets-management layer someone else patches and scales. You write YAML, push it, and the infrastructure underneath is not your problem until you hit its limits — concurrency caps, minutes-based billing, or self-hosted runners that you now have to babysit anyway. Tekton has no equivalent hosted offering for the core project — you are the one running the controllers, the pipeline definitions, and (usually) Tekton Triggers or Tekton Dashboard on top, which means you own upgrades, controller crash loops, and PVC cleanup for orphaned workspace volumes.
Tekton Pipelines gives you Tasks, Pipelines, and Runs — it does not give you a webhook listener or a UI. Both exist (Tekton Triggers, Tekton Dashboard) but ship, version, and upgrade independently of core Pipelines. Budget for that as a second and third piece of software you're operating, not an afterthought.
Where the operational cost actually shows up
The theoretical elegance of "CI is just CRDs" runs into a few concrete costs once you're running it for real. PipelineRuns and TaskRuns accumulate as objects in etcd — without a pruning policy (via the results retention config or a periodic garbage-collection CronJob) you'll eventually notice `kubectl get pipelinerun` taking longer to return, or worse, etcd growing. Workspaces backed by PVCs need a storage class that supports the access mode you want, and if you're running parallel Tasks that all need read access to the same cloned source, you'll spend real time working out whether you want `ReadWriteMany` storage or just re-cloning per Task. None of this is unsolvable, but it is work that a hosted CI product has already done for you and hidden behind a "concurrent jobs" setting.
Every step in a Task step list shares a pod, but every Task in a Pipeline gets its own pod by default. A Pipeline with fifteen small Tasks means fifteen pod scheduling cycles, fifteen image pulls (unless cached on the node), and fifteen sets of resource requests fighting for scheduling. Collapsing related steps into fewer, larger Tasks cuts scheduling latency more than any other single change.
When it's actually worth running yourself
Tekton earns its keep when you're already deep in Kubernetes-native tooling and want CI/CD to be a first-class citizen of that world — building your own internal developer platform, wiring pipeline execution into a custom operator, or needing pipeline definitions that are versioned, composed, and reused across dozens of unrelated repos the way you'd reuse a Helm chart. It's also the right call when your compliance or air-gapped environment genuinely rules out a hosted SaaS CI, and you were going to run self-hosted runners for GitHub Actions anyway — in that world, Tekton's pods-as-runners model is arguably less to operate than a fleet of long-lived Actions runners.
It is not worth it for a small team that just wants builds to run reliably and doesn't have someone whose job includes "the CI system's infrastructure." If your honest answer to "who owns the Tekton controllers when they crash-loop after a Kubernetes upgrade" is nobody in particular, that's the answer: use GitHub Actions or GitLab CI, or Jenkins if you already have it running, and revisit Tekton only when the platform team building the internal developer platform actually exists.
Wrapping up
Tekton is what CI/CD looks like when you take "everything is a Kubernetes object" seriously — Tasks, Pipelines, and Runs are all just CRDs you can inspect, compose, and build tooling around, and that consistency is genuinely valuable if you're building a platform on top of it. But that same design means you inherit all the operational surface of running another controller in your cluster: garbage collection for Runs, storage classes for workspaces, and separate lifecycle management for Triggers and Dashboard. Reach for it when Kubernetes-native composition is the actual requirement, not because a Jenkins install feels dated — for most teams, a hosted CI still gets builds running with less of your own attention spent keeping the CI system itself alive.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.