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 storageclassand look for(default). kubectlconfigured 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:
| Resource | What it is |
|---|---|
Task | A reusable list of steps; each step is a container. A Task runs as one pod. |
TaskRun | One execution of a Task with concrete parameters. |
Pipeline | An ordered graph of Tasks that can share workspaces and pass results. |
PipelineRun | One execution of a Pipeline. |
EventListener, TriggerBinding, TriggerTemplate | Tekton 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.
NoteFor production, pin a specific version instead of
latestso upgrades happen only when you decide. Replacelatestin the URL with a tag such asprevious/v1.16.0, taken from the Tekton Pipelines releases page.
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.
TipTekton's default affinity assistant schedules all pods that share a
ReadWriteOnceworkspace on the same node, which is why both Tasks can mount the same PVC.
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.