Steps & Tasks
One Task, one Pod, steps in order
tekton-tasktekton-stepstep-entrypointstep-init-preparestep-on-errorstep-exit-codestep-timeoutone Pod: init containers, then every step container at once
t = 0.0s- step-clonecontainer not started (init containers running)
- step-buildcontainer not started (init containers running)
- step-testcontainer not started (init containers running)
- step-pushcontainer not started (init containers running)
shared /tekton volume
- /tekton/bin/entrypoint
- /tekton/scripts/script-0-k2x9q
- /tekton/scripts/script-1-p7m3v
- /tekton/scripts/script-2-c4n8d
- /tekton/scripts/script-3-w1r5t
- /tekton/downward/readyPod ready: step 0 may start
- /tekton/steps/step-clone/exitCode0
- /tekton/run/0/outnext step: go
- /tekton/steps/step-build/exitCode0
- /tekton/run/1/outnext step: go
- /tekton/steps/step-test/exitCode0
- /tekton/run/2/outnext step: go
- /tekton/steps/step-push/exitCode0
- /tekton/run/3/outnext step: go
apiVersion: tekton.dev/v1kind: Taskmetadata: {name: build-and-push}spec:steps:- name: cloneimage: alpine/gitscript: git clone $(params.url) .- name: buildimage: golang:1.23script: go build -o app ./...- name: testimage: golang:1.23timeout: 2.5sscript: go test ./...- name: pushimage: gcr.io/go-containerregistry/cranescript: crane push app.tar $(params.image)
Try: play once clean, then make build exit 1. Push and test containers were started all along but never run their commands: their entrypoints see out.err and give up. Switch build to onError: continue and the Pod succeeds, with exitCode 1 still recorded for the step.
- name: buildimage: golang:1.23script: |go vet ./...go build -o /workspace/out/app ./cmd/app
#!/bin/shset -ego vet ./...go build -o /workspace/out/app ./cmd/app
- name: step-buildimage: golang:1.23command: ["/tekton/bin/entrypoint"]args:- -wait_file- /tekton/run/0/out- -post_file- /tekton/run/1/out- -termination_path- /tekton/termination- -step_metadata_dir- /tekton/run/1/status- -entrypoint- /tekton/scripts/script-1-p7m3vvolumeMounts:- {name: tekton-internal-bin, mountPath: /tekton/bin, readOnly: true}- {name: tekton-internal-scripts, mountPath: /tekton/scripts}- {name: tekton-internal-run-0, mountPath: /tekton/run/0, readOnly: true}- {name: tekton-internal-run-1, mountPath: /tekton/run/1}- {name: tekton-internal-workspace, mountPath: /workspace}
No shebang, so Tekton prepends #!/bin/sh and set -e: the first failing command fails the step.
Try: flip between the four modes and watch only the -entrypoint argument change. Whatever you write, the container's real command is always /tekton/bin/entrypoint, the wrapper that enforces step order and records exit codes.
one Pod, one network namespace: steps reach the sidecar at localhost:5432
t = 0.0sspec:sidecars:- name: postgresimage: postgres:17env: [{name: POSTGRES_PASSWORD, value: test}]readinessProbe:exec: {command: [sh, -c, "pg_isready -h localhost"]}steps:- name: migratescript: psql -h localhost -f schema.sql- name: testscript: go test ./... -tags=integration- name: report
Try: untick await-sidecar-readiness and tick slow start: the first step races the database and gets connection refused. Then untick "Tekton stops sidecars": a Pod whose sidecar never exits never completes, which is why the controller swaps its image for nop once the steps are done.
stepTemplate: defaults merged into every step
step-templateedit the stepTemplate
blue: inherited from the template · amber: the step's own value replacing the template's · white: only in the step.
- name: lintimage: golangci/golangci-lint:v2.1 # step overrides templateworkingDir: /workspace/source # ← stepTemplateenv:- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate- {name: CGO_ENABLED, value: "0"} # ← stepTemplatecomputeResources:requests:cpu: 500m # ← stepTemplatememory: 512Mi # ← stepTemplatescript: golangci-lint run- name: buildimage: golang:1.23 # ← stepTemplateworkingDir: /workspace/source # ← stepTemplateenv:- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate- {name: CGO_ENABLED, value: "1"} # step overrides template- {name: GOOS, value: "linux"}computeResources:requests:cpu: 500m # ← stepTemplatememory: 512Mi # ← stepTemplatescript: go build -o app ./cmd/app- name: testimage: golang:1.23 # ← stepTemplateworkingDir: /workspace/source # ← stepTemplateenv:- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate- {name: CGO_ENABLED, value: "0"} # ← stepTemplatecomputeResources:requests:cpu: 500m # ← stepTemplatememory: 512Mi # ← stepTemplatescript: go test -race ./...
Try: clear the template image. lint still has one, but build and test now have none. Then tick "test asks for 2Gi": memory is replaced but cpu still comes from the template, because the merge works field by field and env entries merge by name.
two steps in one Pod
Lands on a shared volume. Press run.
spec:workspaces: [{name: source}]results: [{name: digest}]volumes: [{name: cache, emptyDir: {}}]steps:- name: onescript: echo v1.2 > /workspace/source/VERSION- name: twoscript: cat /workspace/source/VERSION
Try: export a variable, then write a file to /tmp, then use the Workspace. Steps share volumes, not process state: anything that is not a file on a mounted path disappears when the step's container exits.
CPU actually used vs reserved for the Pod
| step-clone | 100m | as written |
| step-build | 1.50 | as written |
| step-test | 1.00 | as written |
| Pod effective | 2.60 | max(sum of containers, largest init) |
kind: Taskspec:steps:- name: clonecomputeResources: {requests: {cpu: "100m"}}- name: buildcomputeResources: {requests: {cpu: "1.50"}}- name: testcomputeResources: {requests: {cpu: "1.00"}}
Try: with per-step requests on a 2-CPU node, the Pod asks for 2.60 although it never uses more than 1.5 and stays Pending. Switch to TaskRun computeResources at 1.5: same steps, the request is split across them so the sum is what one step needs.
apiVersion: tekton.dev/v1beta1kind: StepActionmetadata: {name: git-clone}spec:params:- name: url- {name: revision, default: main}image: alpine/git:2.45.2env:- {name: URL, value: $(params.url)}- {name: REV, value: $(params.revision)}script: |git clone "$URL" /workspace/srcgit -C /workspace/src checkout "$REV"
kind: Taskmetadata: {name: build}spec:steps:- name: fetchref: {name: git-clone}params:- {name: url, value: $(params.repo)}- {name: revision, value: $(params.rev)}- name: compileimage: golang:1.23
kind: Taskmetadata: {name: lint}spec:steps:- name: fetchref: {name: git-clone}params:- {name: url, value: $(params.repo)}- {name: revision, value: main}- name: golangciimage: golangci/golangci-lint:v2.1
# TaskRun status.taskSpec of a run of 'build'steps:- name: fetchimage: alpine/git:2.45.2env:- {name: URL, value: https://github.com/acme/app}- {name: REV, value: v1.4.0}script: |git clone "$URL" /workspace/srcgit -C /workspace/src checkout "$REV"- name: compileimage: golang:1.23
Try: bump the StepAction to the CVE fix. With references, both Tasks pick it up on their next run; with copies, each Task still pins the old image until someone edits it. A ref step may pass params, but not override image, command or script.
- takeawayA Task runs as one Pod. All step containers start together; the injected entrypoint and the files under /tekton/run turn that into a strict sequence with stopAndFail or continue semantics.
- takeawaySteps share only what is mounted: Workspaces, results, /tekton/home and your volumes. Long-lived helpers go in sidecars, which Tekton starts first and stops last.
- takeawayBecause steps are sequential, size the Pod for the largest step, not the sum: use Task-level computeResources or LimitRange defaults rather than stacking per-step requests. More on the TaskRun side in chapter 03.
Key concepts · 20
book chapter 2- Task
A namespaced, reusable definition of steps, sidecars, params, results, workspaces and volumes (tekton.dev/v1).
It is the smallest unit you can run on its own and the building block of Pipelines. Each run of it is one TaskRun and one Pod.
kind: Tasktkn task start build↑ see it in the visualization - Step
One entry in spec.steps: a container image plus a script or command to run in it.
Steps let each part of a job use the image that fits it (git, then golang, then crane) without building one image with every tool.
spec.steps[].imagespec.steps[].name↑ see it in the visualization - The entrypoint binary
A small Tekton program that becomes every step container's real command and wraps the command you wrote.
Kubernetes starts all of a Pod's containers at once. The entrypoint blocks until the previous step's /tekton/run/N/out file appears, then runs your command, then writes its own file and exit code.
/tekton/bin/entrypoint-wait_file-post_file↑ see it in the visualization - prepare and place-scripts
Init containers Tekton adds to the Pod: one copies the entrypoint binary into a shared volume, one writes step scripts to /tekton/scripts.
They run before any step, which is why a TaskRun sits in Pending for a moment even after the Pod is scheduled. Their lifecycle is covered with TaskRuns.
initContainers: prepareinitContainers: place-scripts↑ see it in the visualization - onError
Per-step policy for a non-zero exit: stopAndFail (default) or continue.
With stopAndFail, later steps are skipped and the TaskRun fails. With continue, the exit code is recorded and the next step runs anyway; useful for a lint step whose failure a later step reports.
onError: continueonError: stopAndFail↑ see it in the visualization - Step exit code
The exit status the entrypoint saves for each step under /tekton/steps/step-<name>/exitCode.
Later steps can read it with $(steps.step-<name>.exitCode.path) and decide what to do, and it shows up in the TaskRun's status.steps.
$(steps.step-lint.exitCode.path)status.steps[].terminated.exitCode↑ see it in the visualization - Step timeout
A Go duration on a single step after which the entrypoint kills its process.
A hung step fails fast and later steps are skipped, without waiting for the whole TaskRun's timeout.
spec.steps[].timeout: 5m↑ see it in the visualization - script
Inline script text for a step, written to a file by the place-scripts init container and executed by the entrypoint.
Without a shebang Tekton runs it with #!/bin/sh and set -e. With one (#!/usr/bin/env python3) any interpreter in the image works. A step uses either script or command, not both.
spec.steps[].script#!/usr/bin/env bash↑ see it in the visualization - command and args
The container-style alternative to script: an executable and its arguments.
They are passed to the entrypoint after --. If you give only args, the controller looks up the image's ENTRYPOINT from the registry so it knows what to run.
command: [go]args: [build, ./...]↑ see it in the visualization - Sidecar
A container in spec.sidecars that runs alongside the steps for the whole Task: a database, docker-in-docker, a proxy.
It shares the Pod's network, so steps reach it on localhost. Once all steps finish, the controller replaces the sidecar's image with a nop image so its container exits and the Pod can complete.
spec.sidecars[]nop imagereadinessProbe↑ see it in the visualization - await-sidecar-readiness
A feature flag (default true) that makes the first step wait until every sidecar is ready.
Readiness is signalled through a downward-API file the first step's entrypoint waits on. Turning it off speeds up starts in clusters that cannot provide the volume, at the cost of steps racing their sidecars.
feature-flags: await-sidecar-readiness/tekton/downward/ready↑ see it in the visualization - stepTemplate
A container spec in the Task that every step starts from.
Put shared env, workingDir, image or resource requests in one place. A step's own fields win; env entries merge by name and requests merge key by key.
spec.stepTemplate.envspec.stepTemplate.computeResources↑ see it in the visualization - /tekton/results
The shared directory a step writes a result into, via $(results.<name>.path).
At the end of the Pod the controller collects these small values into the TaskRun status so other Tasks can use them.
$(results.digest.path)↑ see it in the visualization - workingDir
The directory a step's command starts in.
A cd in one step does not carry to the next, so set workingDir on each step, or once in stepTemplate.
workingDir: $(workspaces.source.path)↑ see it in the visualization - Step resource requests
computeResources requests and limits on each step container.
Kubernetes sums the requests of all containers, but steps run one at a time, so per-step requests over-reserve. Older Tekton releases zeroed all but the largest step's request; current ones leave explicit requests alone.
spec.steps[].computeResources.requests.cpu↑ see it in the visualization - Task-level compute resources
A request on the TaskRun (beta) that Tekton splits across the step containers.
The Pod then reserves what the Task needs at any one moment. Steps without requests in a namespace with a LimitRange get its default divided by the number of steps, never below the LimitRange minimum.
TaskRun.spec.computeResourcesLimitRange defaultRequest↑ see it in the visualization - StepAction
A stable resource holding one reusable step (image, params, env, script) that Task steps reference with ref.
Share a git-clone or upload step across many Tasks and update it in one place. The referencing step passes params but cannot override image or script, and StepAction scripts take params through env, not direct substitution.
kind: StepActionsteps[].ref.namesteps[].ref.resolver↑ see it in the visualization - ClusterTask (deprecated)
A cluster-scoped Task kind from the v1beta1 era, deprecated and removed in recent releases.
To share a Task across namespaces now, keep it in one namespace and reference it with the cluster resolver, or fetch it from git, a bundle or the Hub.
taskRef.resolver: cluster↑ see it in the visualization - displayName and description
Optional human-readable labels on Tasks, steps and params.
UIs like the Dashboard show them instead of the DNS-safe name; they change nothing about execution.
spec.steps[].displayNamespec.description↑ see it in the visualization