02

Steps & Tasks

A Task is an ordered list of steps, each one a container image and a command. When it runs, the whole Task becomes a single Pod with one container per step. Kubernetes has no notion of running a Pod's containers in sequence, so Tekton supplies one: an entrypoint binary injected into every step that waits its turn. This chapter looks at that Pod from the inside: ordering and failure, scripts, sidecars, templates, what steps can and cannot share, how resource requests add up, and reusable StepActions.
Two init containers prepare the Pod, then every step container starts at the same moment. Each one runs the entrypoint, which blocks on the previous step's file in /tekton/run. Only when step N-1 writes out does step N run its command. A failure writes out.err instead, and every later entrypoint gives up without running anything.
build onError

one Pod: init containers, then every step container at once

t = 0.0s
0s5s10s15sprepareplace-scriptsstep-clonestep-buildstep-teststep-push
  • 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)
Pod phase
Pending
TaskRun Succeeded
Unknown
Pending

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
task.yaml · highlighted: the step whose command is running now
apiVersion: tekton.dev/v1
kind: Task
metadata: {name: build-and-push}
spec:
steps:
- name: clone
image: alpine/git
script: git clone $(params.url) .
- name: build
image: golang:1.23
script: go build -o app ./...
- name: test
image: golang:1.23
timeout: 2.5s
script: go test ./...
- name: push
image: gcr.io/go-containerregistry/crane
script: 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.

script vs command: what the container really runs

step-scriptstep-command-args
Whatever you write in a step, Tekton rewrites the container so its command is /tekton/bin/entrypoint and your program becomes an argument. A script is first written to a file by the place-scripts init container; command and args are passed through.
what you write (spec.steps[1])
- name: build
image: golang:1.23
script: |
go vet ./...
go build -o /workspace/out/app ./cmd/app
place-scripts init container writes /tekton/scripts/script-1-p7m3v
#!/bin/sh
set -e
go vet ./...
go build -o /workspace/out/app ./cmd/app
what the Pod gets (kubectl get pod -o yaml)
- name: step-build
image: golang:1.23
command: ["/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-p7m3v
volumeMounts:
- {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.

Sidecars: up before the steps, stopped after

task-sidecarawait-sidecar-readiness
A sidecar runs next to the steps for the life of the Pod, sharing its network namespace. Tekton holds the first step until sidecars report ready, and once the last step is done it patches each sidecar's image to a nop image so the container exits.

one Pod, one network namespace: steps reach the sidecar at localhost:5432

t = 0.0s
0s4s8s12s16sinitsidecar-postgresstep-migratestep-teststep-report
sidecar
not started
Pod phase
Pending
task.yaml
spec:
sidecars:
- name: postgres
image: postgres:17
env: [{name: POSTGRES_PASSWORD, value: test}]
readinessProbe:
exec: {command: [sh, -c, "pg_isready -h localhost"]}
steps:
- name: migrate
script: psql -h localhost -f schema.sql
- name: test
script: 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-template
Repeating the same env, workingDir and requests in every step is noise. stepTemplate is a container spec that each step is merged onto: a field the step sets wins, a field it leaves out is inherited. Edit the template and watch the effective steps change.

edit the stepTemplate

blue: inherited from the template · amber: the step's own value replacing the template's · white: only in the step.

effective steps after merging (what the containers get)
- name: lint
image: golangci/golangci-lint:v2.1 # step overrides template
workingDir: /workspace/source # ← stepTemplate
env:
- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate
- {name: CGO_ENABLED, value: "0"} # ← stepTemplate
computeResources:
requests:
cpu: 500m # ← stepTemplate
memory: 512Mi # ← stepTemplate
script: golangci-lint run
- name: build
image: golang:1.23 # ← stepTemplate
workingDir: /workspace/source # ← stepTemplate
env:
- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate
- {name: CGO_ENABLED, value: "1"} # step overrides template
- {name: GOOS, value: "linux"}
computeResources:
requests:
cpu: 500m # ← stepTemplate
memory: 512Mi # ← stepTemplate
script: go build -o app ./cmd/app
- name: test
image: golang:1.23 # ← stepTemplate
workingDir: /workspace/source # ← stepTemplate
env:
- {name: GOFLAGS, value: "-mod=vendor"} # ← stepTemplate
- {name: CGO_ENABLED, value: "0"} # ← stepTemplate
computeResources:
requests:
cpu: 500m # ← stepTemplate
memory: 512Mi # ← stepTemplate
script: 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.

Steps are separate containers. Each has its own root filesystem from its own image, its own environment and its own processes. They see each other's work only through volumes mounted in both: Workspaces, /tekton/results, /tekton/home and volumes you declare. Pick something for step one to do and see whether step two finds it.

two steps in one Pod

step-oneown container
/ /tmp (image rootfs)
env vars
processes
cwd
$ echo v1.2 > /workspace/source/VERSION
shared volumes (mounted in both)
/workspace/source
/tekton/results
/tekton/home
/cache (emptyDir)
step-twoown container
/ /tmp (image rootfs)
env vars
processes
cwd
$ cat /workspace/source/VERSION

Lands on a shared volume. Press run.

task.yaml
spec:
workspaces: [{name: source}]
results: [{name: digest}]
volumes: [{name: cache, emptyDir: {}}]
steps:
- name: one
script: echo v1.2 > /workspace/source/VERSION
- name: two
script: 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.

This chapter keeps Workspaces and results at the overview level. How Workspaces bind to PVCs, ConfigMaps and Secrets is in chapter 06; how results travel between Tasks is in chapter 05.

Resource requests when steps run one at a time

step-resources-sequentialtask-level-compute
The scheduler reserves the sum of all containers' requests, because normally a Pod's containers run together. A Task's steps never do, so naive per-step requests reserve far more than the Pod ever uses, and can make it unschedulable.

CPU actually used vs reserved for the Pod

in use (one step at a time)Pod request (what the scheduler reserves)
01.002.000510secondsnode allocatable 2.00
step-clone100mas written
step-build1.50as written
step-test1.00as written
Pod effective2.60max(sum of containers, largest init)
Pod request
2.60
peak in use
1.50
reserved but idle
63%
schedulable
Pending
Insufficient cpu
kind: Task
spec:
steps:
- name: clone
computeResources: {requests: {cpu: "100m"}}
- name: build
computeResources: {requests: {cpu: "1.50"}}
- name: test
computeResources: {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.

StepActions: reusable steps

step-actioncluster-task
Tasks are reusable, but until StepActions the steps inside them were copy-pasted. A StepAction is its own resource holding one step; a Task's step points at it with ref (by name, or through a resolver) and passes params. At run time the controller inlines it.
git-clone image
stepaction.yaml
apiVersion: tekton.dev/v1beta1
kind: StepAction
metadata: {name: git-clone}
spec:
params:
- name: url
- {name: revision, default: main}
image: alpine/git:2.45.2
env:
- {name: URL, value: $(params.url)}
- {name: REV, value: $(params.revision)}
script: |
git clone "$URL" /workspace/src
git -C /workspace/src checkout "$REV"
task-build.yaml
kind: Task
metadata: {name: build}
spec:
steps:
- name: fetch
ref: {name: git-clone}
params:
- {name: url, value: $(params.repo)}
- {name: revision, value: $(params.rev)}
- name: compile
image: golang:1.23
task-lint.yaml
kind: Task
metadata: {name: lint}
spec:
steps:
- name: fetch
ref: {name: git-clone}
params:
- {name: url, value: $(params.repo)}
- {name: revision, value: main}
- name: golangci
image: golangci/golangci-lint:v2.1
what the controller inlines at run time
# TaskRun status.taskSpec of a run of 'build'
steps:
- name: fetch
image: alpine/git:2.45.2
env:
- {name: URL, value: https://github.com/acme/app}
- {name: REV, value: v1.4.0}
script: |
git clone "$URL" /workspace/src
git -C /workspace/src checkout "$REV"
- name: compile
image: 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
  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. What steps share

    Mounted volumes: workspaces under /workspace, /tekton/results, /tekton/home, and any volumes you mount; not env vars, processes, or the image filesystem.

    Each step is a separate container, so the only way to hand something on is to write it to a path both steps mount. Anything else is gone when the step exits.

    /workspace/<name>spec.volumes[].emptyDirspec.steps[].volumeMounts
    ↑ see it in the visualization
  14. /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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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