Alright, let’s talk Tekton. If you’ve been slapping Jenkins or GitLab runners onto your Kubernetes cluster and hoping for the best, you’re going to love this. Tekton is different. It’s not just another CI/CD tool that runs on Kubernetes; it’s a framework built from Kubernetes resources. This isn’t a square peg in a round hole. It’s a round peg made of the same wood as the hole. The entire pipeline—every task, every step—is defined and runs as a Kubernetes Pod. This means you get to use kubectl to manage your CI/CD infrastructure. No more bespoke APIs or weird configuration languages. It’s all just YAML, my friend. Beautiful, terrifying, powerful YAML.

Now, the mental model. Tekton introduces a few key Custom Resource Definitions (CRDs) that you need to wrap your head around. Don’t worry, they make sense once you see them in action.

The Core Building Blocks

First, a Task. This is your smallest unit of work, a single function. It contains one or more Steps. Each Step is a container image that does one specific thing, like run a test or build a binary. A TaskRun is the actual execution of a Task. It’s the instance, the Pod that gets created.

Then, you have a Pipeline. This is a composition of Tasks, defining the order they run in (sequentially or in parallel) and what data passes between them. A PipelineRun is, you guessed it, the instance of a Pipeline being executed.

This separation of definition (Task, Pipeline) from execution (TaskRun, PipelineRun) is brilliant. It means you can define your CI/CD logic once and run it a million times with different parameters, all tracked as first-class Kubernetes objects.

Let’s build a simple Task to run some unit tests. You’d apply this with kubectl apply -f:

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: unit-test
spec:
  params:
    - name: app-repo-url
      type: string
      description: The URL of the git repo to clone
  steps:
    - name: clone-and-test
      image: golang:1.21
      script: |
        # Clone the repository
        git clone $(params.app-repo-url) /workspace/source
        cd /workspace/source
        # Run the tests. The WORKSPACE is a shared volume between steps.
        go test ./...

See? It’s just a Pod spec at heart. But now, to run it, you create a TaskRun:

apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
  name: unit-test-run-1
spec:
  taskRef:
    name: unit-test
  params:
    - name: app-repo-url
      value: "https://github.com/myorg/myapp.git"

Watch it go with kubectl get taskruns and kubectl get pods. It’s magic. But the real power comes from chaining these together.

Wiring Things Together with Pipelines and Workspaces

The example above has a glaring problem. It clones the repo itself. What if the next Task needs that same code? You don’t want to clone it again. This is where Workspaces come in. Think of them as shared volumes that you can pass between Tasks in a Pipeline. You declare a workspace in your Task and the Pipeline handles mounting it (often a PersistentVolumeClaim).

Let’s refactor. First, a better Task that expects its source code to be provided:

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: run-unit-tests
spec:
  workspaces:
    - name: source
      description: The git repository to run tests against.
  steps:
    - name: test
      image: golang:1.21
      workingDir: $(workspaces.source.path)
      script: |
        go test ./...

Now, let’s create a Pipeline that uses this. It’ll need a Task to clone the repo first, which is so common that Tekton provides a ready-to-use one from their catalog.

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: test-pipeline
spec:
  workspaces:
    - name: shared-workspace
  tasks:
    - name: fetch-source
      taskRef:
        name: git-clone
        kind: ClusterTask # This is a pre-installed cluster-scoped Task
      params:
        - name: url
          value: "https://github.com/myorg/myapp"
      workspaces:
        - name: output
          workspace: shared-workspace

    - name: test-application
      runAfter: [fetch-source] # Explicitly define order
      taskRef:
        name: run-unit-tests
      workspaces:
        - name: source
          workspace: shared-workspace # Same workspace!

The git-clone ClusterTask writes the code to its output workspace, which is our pipeline’s shared-workspace. The test-application task then uses that same workspace. No redundant clones, and everything is neatly isolated.

The Rough Edges and Pitfalls

It’s not all sunshine and rainbows. Tekton is powerful, but that power comes with complexity.

  1. YAML Fatigue: You will write. So. Much. YAML. A full pipeline can be a behemoth. This is where a tool like the Tekton CLI (tkn) becomes your best friend for interacting with runs without drowning in kubectl output.
  2. Debugging: When a PipelineRun fails, you’re debugging a tree of Pods. You have to find the specific failed Pod, then the specific failed container within it, and check its logs. tkn pipelinerun logs -f helps, but it’s still more complex than a traditional CI/CD server’s linear log output.
  3. The Catalog is a Mixed Bag: The community ClusterTask catalog is fantastic for common tasks (git-clone, buildpacks, etc.), but the versions can be inconsistent. Always check the docs for the specific ClusterTask you’re using—don’t assume the parameters.
  4. Workspace Binding: This is a common stubbing point. When you create a PipelineRun, you must explicitly bind the pipeline’s workspaces. You can’t just run it. For a shared volume, you need to provide a PVC. This is a classic “it’s flexible but now you have to make a decision” moment.
# This PipelineRun MUST bind the workspace
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  generateName: test-pipeline-run-
spec:
  pipelineRef:
    name: test-pipeline
  workspaces:
    - name: shared-workspace
      volumeClaimTemplate: # Tekton will create a PVC on the fly for this run
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi

The beauty is that once you get this, you have a CI/CD system that scales with your cluster, speaks native Kubernetes, and gives you an immense amount of control. You’re not fighting a foreign tool; you’re extending your platform. And that, if you ask me, is exactly how it should be.