Tekton Pipelines

Tekton is the k8s-native CI/CD framework. Where Argo Workflows is general-purpose, Tekton is purpose-built for CI/CD. Pipelines are made of Tasks, Pipelines, and PipelineRuns. Every step runs in a pod. You get triggers, workspaces, and a real k8s resource model.

Tekton vs Argo Workflows

TektonArgo Workflows
PurposeCI/CDGeneral workflows
ModelTasks + PipelinesTemplates + DAGs
TriggersBuilt-in (Tekton Triggers)Separate (Argo Events)
Result trackingPipelineResources (deprecated), OCI artifactsArtifacts
CLItknargo
UITekton DashboardArgo Workflows UI
EcosystemTekton Hub (reusable tasks)Templates
MaturityGA in 2023Mature

For CI/CD specifically: Tekton’s model is more focused. For batch jobs, ML, etc., Argo Workflows is more flexible.

The mental model

PipelineRun (a specific execution)
   │
   └── Pipeline (the template)
          │
          ├── Task 1 (clone)
          ├── Task 2 (test)        ← depends on Task 1
          ├── Task 3 (build)       ← depends on Task 2
          ├── Task 4 (scan)        ← depends on Task 3
          └── Task 5 (push)        ← depends on Task 4
                                       │
                                       └── Each Task runs Steps in containers
                                              │
                                              ├── Step 1 (git clone)
                                              ├── Step 2 (configure)
                                              └── Step 3 (run)
  • Task: a unit of work (one or more steps)
  • Step: a single command in a container
  • Pipeline: a DAG of Tasks
  • PipelineRun: an execution of a Pipeline

A simple CI pipeline

# Task: git-clone
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: git-clone
spec:
  params:
    - name: url
    - name: revision
      default: main
  workspaces:
    - name: output
  steps:
    - name: clone
      image: alpine/git:v2.42.0
      script: |
        git clone $(params.url) $(workspaces.output.path)
        cd $(workspaces.output.path)
        git checkout $(params.revision)
# Task: build-and-push
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: build-and-push
spec:
  params:
    - name: IMAGE
    - name: TAG
  workspaces:
    - name: source
  steps:
    - name: build
      image: gcr.io/kaniko-project/executor:debug
      workingDir: $(workspaces.source.path)
      script: |
        /kaniko/executor \
          --context=$(workspaces.source.path) \
          --dockerfile=$(workspaces.source.path)/Dockerfile \
          --destination=$(params.IMAGE):$(params.TAG) \
          --cache=true
# Pipeline
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: build-and-deploy
spec:
  params:
    - name: git-url
    - name: image-name
    - name: image-tag
  workspaces:
    - name: shared
  tasks:
    - name: clone
      taskRef:
        name: git-clone
      params:
        - name: url
          value: $(params.git-url)
      workspaces:
        - name: output
          workspace: shared
 
    - name: test
      runAfter: [clone]
      taskSpec:
        workspaces:
          - name: source
        steps:
          - name: test
            image: golang:1.21
            workingDir: $(workspaces.source.path)
            script: |
              go test ./...
      workspaces:
        - name: source
          workspace: shared
 
    - name: build
      runAfter: [test]
      taskRef:
        name: build-and-push
      params:
        - name: IMAGE
          value: $(params.image-name)
        - name: TAG
          value: $(params.image-tag)
      workspaces:
        - name: source
          workspace: shared
# PipelineRun
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: build-myapp-
spec:
  pipelineRef:
    name: build-and-deploy
  params:
    - name: git-url
      value: https://github.com/myorg/myapp
    - name: image-name
      value: myregistry/myapp
    - name: image-tag
      value: v1.2.3
  workspaces:
    - name: shared
      volumeClaimTemplate:
        spec:
          accessModes: [ReadWriteOnce]
          resources:
            requests:
              storage: 5Gi

Run:

kubectl apply -f pipelinerun.yaml

Tasks and Steps

Task

A Task is a reusable template. The Pod is created with one container per Step.

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: my-task
spec:
  params:
    - name: image-name
      type: string
      default: myapp
  workspaces:
    - name: source
  results:
    - name: image-digest
      description: The digest of the built image
  steps:
    - name: build
      image: gcr.io/kaniko-project/executor:debug
      script: |
        /kaniko/executor ...
    - name: report
      image: alpine:3.19
      script: |
        echo "$(steps.build.results.digest)" > $(results.image-digest.path)

Task can be inline (taskSpec:) or referenced (taskRef: { name: ... }).

Steps

Steps are commands in containers. They run sequentially in the same Pod.

steps:
  - name: install
    image: golang:1.21
    script: go mod download
 
  - name: test
    image: golang:1.21
    script: go test ./...
 
  - name: build
    image: gcr.io/kaniko-project/executor:debug
    script: /kaniko/executor ...

Each Step:

  • Runs in its own container
  • Can have its own image
  • Has its own working dir
  • Can mount workspaces

Workspaces

Workspaces are shared volumes between Tasks.

# Task definition
spec:
  workspaces:
  - name: source
    description: Source code
    mountPath: /workspace
 
# Pipeline definition
spec:
  workspaces:
  - name: shared
    description: Shared workspace
 
  tasks:
  - name: clone
    taskRef: { name: git-clone }
    workspaces:
    - name: output
      workspace: shared
 
  - name: test
    runAfter: [clone]
    taskRef: { name: run-tests }
    workspaces:
    - name: source
      workspace: shared
 
# PipelineRun
spec:
  workspaces:
  - name: shared
    volumeClaimTemplate:
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 5Gi

Types of workspaces:

  • EmptyDir — fast, ephemeral (default in some configs)
  • PersistentVolumeClaim — persistent across runs
  • ConfigMap / Secret — for config
  • Existing PVC — for sharing with other k8s resources

Parameters

# Task
spec:
  params:
    - name: image-tag
      type: string
      description: The image tag
  steps:
    - name: build
      image: alpine:3.19
      args: ["build", "$(params.image-tag)"]
# Pipeline
spec:
  params:
    - name: image-tag
  tasks:
    - name: build
      taskRef: { name: build }
      params:
        - name: image-tag
          value: $(params.image-tag)
# PipelineRun
spec:
  params:
    - name: image-tag
      value: "v1.2.3"

Parameter types: string, array, object.

Results

Tasks can output results that other Tasks consume.

# Task: produce
spec:
  results:
    - name: commit-sha
  steps:
    - name: get-commit
      image: alpine/git:v2.42.0
      script: |
        git rev-parse HEAD > $(results.commit-sha.path)
# Pipeline
spec:
  tasks:
    - name: get-commit
      taskRef: { name: produce }
    - name: use-commit
      runAfter: [get-commit]
      taskRef: { name: use }
      params:
        - name: sha
          value: "$(tasks.get-commit.results.commit-sha)"

Tekton Triggers

Event-driven CI. Listens for events (GitHub push, GitLab, etc.) and creates PipelineRuns.

# EventListener
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: github-listener
spec:
  serviceAccountName: tekton-triggers-sa
  triggers:
    - name: github-push
      interceptors:
        - ref:
            name: github
          params:
            - name: eventTypes
              value: [push]
        - ref:
            name: cel
          params:
            - name: filters
              value:
                - "body.ref=='refs/heads/main'"
      bindings:
        - ref: github-push-binding
      template:
        ref: github-push-template
# TriggerTemplate
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: github-push-template
spec:
  params:
    - name: gitrevision
    - name: gitrepositoryurl
  resourcetemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: ci-push-
      spec:
        pipelineRef:
          name: build-and-deploy
        params:
          - name: git-url
            value: $(tt.params.gitrepositoryurl)
          - name: image-tag
            value: $(tt.params.gitrevision)

The EventListener exposes an HTTP endpoint. Configure GitHub webhook to point to it.

Tekton Hub (reusable tasks)

The community-maintained catalog of Tasks:

# install the tkn CLI
brew install tektoncd-cli
 
# install a task from the hub
tkn hub install task git-clone
tkn hub install task buildah
tkn hub install task knative-deploy

Reusable Tasks for common operations (git, buildah, kaniko, s3, etc.).

Auth: Service Accounts and Secrets

Tekton Tasks need credentials for git, registries, etc.

# Secret for registry
apiVersion: v1
kind: Secret
metadata:
  name: registry-creds
  namespace: tekton-pipelines
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64-of-dockerconfig>
---
# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-sa
  namespace: tekton-pipelines
secrets:
  - name: registry-creds
# Task
spec:
  steps:
    - name: push
      image: buildah
      script: buildah push ...
      # service account auto-mounts the secret

For cloud registries, use workload identity (AWS IRSA, GKE WI).

Tekton vs Jenkins (brief)

TektonJenkins
Architecturek8s-native, declarativeMaster + agents
StateNone (in k8s)Heavy (Jenkins home)
PluginsTasks (k8s resources)Many plugins, fragile
UITekton DashboardJenkins UI
GitOps-friendlyYes (CRDs in git)Hard (XML/JCasC)
ScalePods scale, no master bottleneckMaster bottleneck
Maturity2020+2010s+

Use Tekton: for new k8s-native CI/CD, especially if you’re already GitOps-pilled.

Use Jenkins: if you have an existing investment, legacy pipelines, or need plugins Tekton doesn’t have.

Common gotchas

  • Steps run sequentially in one pod. If you need parallelism, use multiple Tasks.
  • Workspaces are mounted in each Task pod. Make sure the Task spec mounts them.
  • Results are passed via files. A Task writes to a path, another Task reads.
  • Service account permissions are key. Tekton Tasks run as the SA, not the user.
  • Triggers require interceptors to filter events. Without them, every push triggers a run.
  • Hub tasks are versioned. Pin a specific version, not “latest.”
  • PipelineRun timeout is set per-PipelineRun, not on the Pipeline.
  • Retries are set per-Task, not per-Pipeline.
  • Workspaces can’t be shared across PipelineRuns unless you use a PVC.
  • Triggers use CEL for filtering (or interceptors), which is its own learning curve.

A worked CI/CD pipeline

Goal: GitHub push to main → clone → test → build → scan → push → update gitops.

# Pipeline
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: ci
spec:
  params:
    - name: git-url
    - name: git-revision
    - name: image-name
    - name: image-tag
  workspaces:
    - name: shared
  tasks:
    - name: clone
      taskRef: { name: git-clone }
      workspaces:
        - name: output
          workspace: shared
      params:
        - name: url
          value: $(params.git-url)
        - name: revision
          value: $(params.git-revision)
 
    - name: test
      runAfter: [clone]
      taskRef: { name: golang-test }
      workspaces:
        - name: source
          workspace: shared
 
    - name: build
      runAfter: [test]
      taskRef: { name: kaniko }
      workspaces:
        - name: source
          workspace: shared
      params:
        - name: IMAGE
          value: $(params.image-name)
        - name: TAG
          value: $(params.image-tag)
 
    - name: scan
      runAfter: [build]
      taskRef: { name: trivy-scanner }
      params:
        - name: IMAGE
          value: $(params.image-name):$(params.image-tag)
 
    - name: update-gitops
      runAfter: [scan]
      taskRef: { name: update-kustomize }
      params:
        - name: repo
          value: https://github.com/myorg/gitops
        - name: image-tag
          value: $(params.image-tag)

The update-gitops task uses kustomize edit set image to update the manifest and pushes to the gitops repo.

See also

Across the wiki