Tekton is a Kubernetes-native CI/CD framework: every build step runs as a container inside a pod, and pipelines are defined as Kubernetes custom resources that you manage with kubectl. In this tutorial you will install Tekton Pipelines and the tkn CLI, write a Task, chain Tasks into a Pipeline that clones a Git repository into a shared workspace, and add Tekton Triggers so an HTTP webhook starts the pipeline automatically. You will finish by installing the Tekton Dashboard to follow runs in a browser.

Prerequisites

To follow this tutorial you need:

  • A Kubernetes cluster running version 1.28 or later, for example a CubePath managed Kubernetes cluster or a k3s/kubeadm cluster on your own VPS.
  • A default StorageClass in the cluster, because the pipeline in Step 5 requests a PersistentVolumeClaim. Check it with kubectl get storageclass and look for (default).
  • kubectl configured with cluster-admin rights on a workstation running Ubuntu 24.04 (or any Linux/macOS machine).
  • Basic familiarity with Kubernetes objects (pods, namespaces, services).

All examples use the default namespace.

Tekton building blocks

Before writing YAML, it helps to know the five resources you will use:

ResourceWhat it is
TaskA reusable list of steps; each step is a container. A Task runs as one pod.
TaskRunOne execution of a Task with concrete parameters.
PipelineAn ordered graph of Tasks that can share workspaces and pass results.
PipelineRunOne execution of a Pipeline.
EventListener, TriggerBinding, TriggerTemplateTekton Triggers resources that turn an HTTP event into a PipelineRun.

Tasks and Pipelines are templates; nothing runs until you create a TaskRun or PipelineRun.

Step 1 - Installing Tekton Pipelines

Tekton publishes its release manifests at infra.tekton.dev. Apply the latest Pipelines release:

kubectl apply --filename https://infra.tekton.dev/tekton-releases/pipeline/latest/release.yaml

This creates the tekton-pipelines namespace with the controller and webhook, plus the custom resource definitions. Wait until both pods are ready:

kubectl get pods --namespace tekton-pipelines --watch
NAME                                           READY   STATUS    RESTARTS   AGE
tekton-events-controller-6d7f8c9b5d-9xk2p      1/1     Running   0          40s
tekton-pipelines-controller-7c9d8b6f4-lq8vz    1/1     Running   0          40s
tekton-pipelines-webhook-5b8f7d9c6-h2jtm       1/1     Running   0          40s

Press CTRL+C to stop watching. Confirm that the new API types are registered:

kubectl api-resources --api-group=tekton.dev

You should see tasks, taskruns, pipelines and pipelineruns in the list.

Step 2 - Installing the tkn CLI

kubectl is enough to create Tekton resources, but the tkn CLI makes it much easier to start runs and read logs across all pods of a pipeline. The Tekton CLI project ships a .deb package on GitHub. Set the version you want (check the CLI releases page for the latest) and install it:

TKN_VERSION=0.46.1
curl -LO "https://github.com/tektoncd/cli/releases/download/v${TKN_VERSION}/tektoncd-cli-${TKN_VERSION}_Linux-64bit.deb"
sudo apt install "./tektoncd-cli-${TKN_VERSION}_Linux-64bit.deb"

On an arm64 workstation use the Linux-ARM64.deb asset instead. On macOS, brew install tektoncd-cli installs the same tool.

Verify the client and that it can talk to your cluster:

tkn version
Client version: 0.46.1
Pipeline version: v1.16.0

If Pipeline version shows unknown, tkn cannot reach the cluster; check your current kubectl context.

Step 3 - Creating and running your first Task

Start with a Task that has a single step. Create the file:

nano hello-task.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: hello
spec:
  params:
    - name: name
      type: string
      default: World
  steps:
    - name: greet
      image: docker.io/library/alpine:3.20
      script: |
        #!/bin/sh
        echo "Hello, $(params.name)!"

The script field is written to a file inside the container and executed, so you can use normal shell syntax. $(params.name) is substituted by Tekton before the pod starts.

Apply the Task and run it with tkn, which creates a TaskRun for you and streams its logs:

kubectl apply -f hello-task.yaml
tkn task start hello --param name=CubePath --showlog
TaskRun started: hello-run-4xk7q
Waiting for logs to be available...
[greet] Hello, CubePath!

List the runs to see their status:

tkn taskrun list
NAME              STARTED         DURATION   STATUS
hello-run-4xk7q   1 minute ago    9s         Succeeded

Each TaskRun is backed by a normal pod, so kubectl get pods shows a pod named after the run in Completed state.

Step 4 - Passing results between steps

Tasks can publish small values (a commit SHA, an image digest, a version string) as results. Tekton gives each result a file path; whatever a step writes there becomes the result value, and Pipelines can pass it to later Tasks.

Create a Task that clones a repository into a workspace and reports the commit it checked out:

nano git-clone-task.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: git-clone-simple
spec:
  params:
    - name: url
      type: string
    - name: revision
      type: string
      default: main
  workspaces:
    - name: source
      description: The repository is cloned here.
  results:
    - name: commit
      description: SHA of the checked out commit.
  steps:
    - name: clone
      image: docker.io/alpine/git:latest
      workingDir: $(workspaces.source.path)
      script: |
        #!/bin/sh
        set -eu
        git clone --depth 1 --branch "$(params.revision)" "$(params.url)" repo
        cd repo
        printf '%s' "$(git rev-parse HEAD)" > "$(results.commit.path)"
        echo "Cloned $(params.url) at $(git rev-parse --short HEAD)"

A workspace is a volume declared by the Task but supplied by whoever runs it. The Task only knows the mount path ($(workspaces.source.path)); the PipelineRun decides whether it is a PVC, an emptyDir, a ConfigMap or a Secret.

Create a second Task that uses the cloned code. This one just lists the files and counts lines of code, but in a real pipeline it would run your tests or build:

nano inspect-task.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: inspect-source
spec:
  params:
    - name: commit
      type: string
  workspaces:
    - name: source
  steps:
    - name: inspect
      image: docker.io/library/alpine:3.20
      workingDir: $(workspaces.source.path)/repo
      script: |
        #!/bin/sh
        set -eu
        echo "Inspecting commit $(params.commit)"
        ls -la
        find . -type f -not -path './.git/*' | wc -l

Apply both Tasks:

kubectl apply -f git-clone-task.yaml -f inspect-task.yaml
task.tekton.dev/git-clone-simple created
task.tekton.dev/inspect-source created

Step 5 - Building a Pipeline with a shared workspace

Now combine the two Tasks. The Pipeline declares one workspace, hands it to both Tasks, and feeds the commit result of the first Task into the second. runAfter is not needed here because referencing $(tasks.clone.results.commit) already makes inspect wait for clone.

nano pipeline.yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: clone-and-inspect
spec:
  params:
    - name: repo-url
      type: string
    - name: revision
      type: string
      default: main
  workspaces:
    - name: shared-data
  tasks:
    - name: clone
      taskRef:
        name: git-clone-simple
      params:
        - name: url
          value: $(params.repo-url)
        - name: revision
          value: $(params.revision)
      workspaces:
        - name: source
          workspace: shared-data
    - name: inspect
      taskRef:
        name: inspect-source
      params:
        - name: commit
          value: $(tasks.clone.results.commit)
      workspaces:
        - name: source
          workspace: shared-data

Each Task runs in its own pod, so the workspace must be a volume both pods can mount. A volumeClaimTemplate in the PipelineRun creates a fresh PVC for every run and deletes it when the PipelineRun is deleted:

nano pipelinerun.yaml
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: clone-and-inspect-run-
spec:
  pipelineRef:
    name: clone-and-inspect
  params:
    - name: repo-url
      value: https://github.com/tektoncd/catalog.git
  workspaces:
    - name: shared-data
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi

Because the file uses generateName, create it with kubectl create (not apply) so every invocation produces a new run:

kubectl apply -f pipeline.yaml
kubectl create -f pipelinerun.yaml

Follow the logs of the most recent run:

tkn pipelinerun logs --last --follow
[clone : clone] Cloned https://github.com/tektoncd/catalog.git at 3f9a1c2
[inspect : inspect] Inspecting commit 3f9a1c2d8e7b...
[inspect : inspect] total 48
[inspect : inspect] drwxr-xr-x    6 root     root          4096 Sep 25 10:12 .
...
[inspect : inspect] 1873

Check the overall status and the task breakdown:

tkn pipelinerun describe --last

The Status field should read Succeeded, and the Taskruns section lists clone and inspect, both succeeded.

Step 6 - Starting the pipeline from a webhook with Tekton Triggers

Tekton Triggers adds an EventListener: a small HTTP service that receives events (for example a Git provider webhook), extracts fields from the payload, and creates a PipelineRun.

Install Triggers and its core interceptors:

kubectl apply --filename https://infra.tekton.dev/tekton-releases/triggers/latest/release.yaml
kubectl apply --filename https://infra.tekton.dev/tekton-releases/triggers/latest/interceptors.yaml
kubectl get pods --namespace tekton-pipelines

Wait until tekton-triggers-controller, tekton-triggers-webhook and tekton-triggers-core-interceptors are Running.

The EventListener runs with a ServiceAccount that must be allowed to read Triggers resources and create PipelineRuns. Triggers ships two ClusterRoles for this purpose; bind them to a new ServiceAccount:

nano triggers-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tekton-triggers-sa
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tekton-triggers-eventlistener-binding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-sa
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: tekton-triggers-eventlistener-roles
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tekton-triggers-eventlistener-clusterbinding
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-sa
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: tekton-triggers-eventlistener-clusterroles

Next, define the three Triggers resources in one file. The TriggerBinding maps JSON fields from the request body to parameters, the TriggerTemplate turns those parameters into a PipelineRun, and the EventListener ties them together:

nano triggers.yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: push-binding
spec:
  params:
    - name: repo-url
      value: $(body.repository.clone_url)
    - name: revision
      value: $(body.branch)
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: clone-and-inspect-template
spec:
  params:
    - name: repo-url
    - name: revision
      default: main
  resourcetemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: clone-and-inspect-triggered-
      spec:
        pipelineRef:
          name: clone-and-inspect
        params:
          - name: repo-url
            value: $(tt.params.repo-url)
          - name: revision
            value: $(tt.params.revision)
        workspaces:
          - name: shared-data
            volumeClaimTemplate:
              spec:
                accessModes:
                  - ReadWriteOnce
                resources:
                  requests:
                    storage: 1Gi
---
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: push-listener
spec:
  serviceAccountName: tekton-triggers-sa
  triggers:
    - name: on-push
      bindings:
        - ref: push-binding
      template:
        ref: clone-and-inspect-template

Apply both files and check that the EventListener is ready:

kubectl apply -f triggers-rbac.yaml -f triggers.yaml
kubectl get eventlistener push-listener
NAME            ADDRESS                                                   AVAILABLE   REASON                     READY   REASON
push-listener   http://el-push-listener.default.svc.cluster.local:8080   True        MinimumReplicasAvailable   True

Triggers creates a Service named el-<listener name> on port 8080. Forward it to your workstation and send a test event:

kubectl port-forward service/el-push-listener 8080:8080

In a second terminal:

curl -s -X POST http://localhost:8080 \
  -H 'Content-Type: application/json' \
  -d '{"branch": "main", "repository": {"clone_url": "https://github.com/tektoncd/catalog.git"}}'
{"eventListener":"push-listener","namespace":"default","eventListenerUID":"...","eventID":"..."}

A new PipelineRun appears within a second or two:

tkn pipelinerun list
NAME                                  STARTED          DURATION   STATUS
clone-and-inspect-triggered-8s2kd     5 seconds ago    ---        Running
clone-and-inspect-run-7wq9m           10 minutes ago   38s        Succeeded

To receive real webhooks, expose el-push-listener through an Ingress with TLS, and add an interceptor (for example the github interceptor with a webhook secret) so only signed requests from your Git provider can start builds. Never expose an EventListener without request validation.

Step 7 - Installing the Tekton Dashboard

The Tekton Dashboard is a web UI that shows Tasks, Pipelines, runs and their logs. The default release is read-only, which is the safe choice for a UI you reach through port forwarding:

kubectl apply --filename https://infra.tekton.dev/tekton-releases/dashboard/latest/release.yaml
kubectl get pods --namespace tekton-pipelines

When the tekton-dashboard pod is Running, forward the service to your workstation:

kubectl port-forward --namespace tekton-pipelines service/tekton-dashboard 9097:9097

Open http://localhost:9097 in your browser. Under PipelineRuns you will see both the manual and the triggered runs, with per-step logs.

The Dashboard has no authentication of its own. If you publish it through an Ingress, put it behind an authenticating proxy (OAuth2 Proxy, basic auth on the Ingress controller, or your SSO).

Troubleshooting

PipelineRun stuck in Pending or pods stuck in Pending. The workspace PVC could not be bound. Run kubectl get pvc and kubectl describe pvc <name>; usually there is no default StorageClass. Mark one as default or set storageClassName in the volumeClaimTemplate.

tkn shows Failed with couldn't find remote ref main. The branch in revision does not exist in the repository. Pass the correct branch with --param revision=... or in the webhook body.

EventListener returns 202 but no PipelineRun is created. Check the listener logs with kubectl logs deployment/el-push-listener. A forbidden error means the RBAC bindings from Step 6 are missing or bound to the wrong namespace.

Old runs pile up. Every run keeps its pods and PVC until you delete it. Clean up with tkn pipelinerun delete --keep 5 or configure pruning in the Tekton Operator.

Conclusion

You installed Tekton Pipelines, Triggers, the tkn CLI and the Dashboard, and built a pipeline that clones a repository into a shared workspace, passes the commit SHA between Tasks, and starts automatically from an HTTP event. From here you can add a build Task that produces container images (for example with Buildah or Kaniko), store registry credentials in a Secret linked to the pipeline's ServiceAccount, and reuse community Tasks through Tekton's remote resolvers instead of writing every Task yourself.