In the previous articles, we covered Tekton's components and execution model. Now it's time to get hands-on. This article walks through a complete Tekton CI/CD pipeline for a Java Spring application — from source code to a running Knative service.
The pipeline is composed of three reusable Tasks:
- Auto-generate image tag (timestamp-based)
- Maven build + Kaniko image push
- Deploy to Kubernetes via kubectl
You can extend this baseline with additional Tasks — code quality checks, integration tests, security scanning — to build out a production-grade CI/CD system.
1. Prerequisites: Persistent Storage for Maven
Maven downloads dependencies on every build by default. Without a local cache, each pipeline run re-downloads the entire dependency tree — slow and wasteful.
We fix this with a PersistentVolumeClaim mounted into the Maven build container:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: maven-repo-local
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "20Gi"
Size your PVC based on your actual Maven local repository. 20Gi is a reasonable starting point for most projects.
2. Pipeline Resources: Git Repo & Image Registry
Define the input (Git source) and output (container image) for the pipeline.
# Input: Git repository
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: git-resource-helloworld-java-spring
spec:
type: git
params:
- name: url
value: https://github.com/knativebook/helloworld-java-spring.git
- name: revision
value: master
---
# Output: Container image destination
# Note: image tag is auto-generated at runtime — no need to specify it here
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: image-resource-helloworld-java-spring
spec:
type: image
params:
- name: url
value: docker.io/{username}/helloworld-java-spring
3. Task 1: Auto-Generate Image Tag (Reusable)
Instead of hardcoding image tags, this Task generates a timestamp-based tag at runtime (e.g. 20240519-143022) and writes it to a Tekton Result for downstream Tasks to consume.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: generate-image-tag
spec:
resources:
outputs:
- name: builtImage
type: image
results:
- name: timestamp
description: Generated image tag (timestamp format)
steps:
- name: get-timestamp
image: bash:latest
script: |
#!/usr/bin/env bash
ts=$(date "+%Y%m%d-%H%M%S")
echo "Current Timestamp: ${ts}"
echo "Image URL: $(resources.outputs.builtImage.url):${ts}"
echo -n "$(resources.outputs.builtImage.url):${ts}" | tee $(results.timestamp.path)
volumeMounts:
- name: localtime
mountPath: /etc/localtime
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
Key points:
- The generated image URL (with tag) is written to
$(results.timestamp.path) - Downstream Tasks reference it via
$(tasks.generate-image-url.results.timestamp) - Timezone is mounted from the host to ensure consistent timestamp formatting
- ✅ This Task is reusable across any Pipeline that needs timestamp-based tagging
4. Task 2: Maven Build + Kaniko Image Push
This Task has two Steps:
-
maven-compile— builds the Java project, skipping tests for speed -
build-and-push— uses Kaniko to build and push the image without requiring Docker daemon access
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: maven-build-and-push
spec:
params:
- name: imageUrl
type: string
resources:
inputs:
- name: git-source
type: git
steps:
- name: maven-compile
image: maven:3.6.1-jdk-8-alpine
workingDir: "$(resources.inputs.git-source.path)"
command: ['/usr/bin/mvn']
args:
- 'clean'
- 'install'
- '-Dmaven.test.skip=true'
volumeMounts:
- name: maven-repository
mountPath: /root/.m2
- name: build-and-push
image: gcr.io/kaniko-project/executor:debug-v0.24.0
env:
- name: "DOCKER_CONFIG"
value: "/tekton/home/.docker/"
command:
- /kaniko/executor
args:
- --dockerfile=$(resources.inputs.git-source.path)/Dockerfile
- --destination=$(params.imageUrl)
- --context=$(resources.inputs.git-source.path)
- --log-timestamp
volumeMounts:
- name: localtime
mountPath: /etc/localtime
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
- name: maven-repository
persistentVolumeClaim:
claimName: maven-repo-local
Key points:
-
maven-compilemounts the PVC at/root/.m2— Maven's local cache directory -
build-and-pushuses Kaniko, which builds images inside a container without a Docker socket — safe for Kubernetes environments - Docker registry credentials are expected at
/tekton/home/.docker/via a mounted Secret (configured in the ServiceAccount) -
$(params.imageUrl)receives the full image URL + tag from Task 1's Result
5. Task 3: Deploy to Kubernetes (Reusable)
This Task deploys the built image as a Knative Service using kubectl apply. It has two Steps:
-
create-ksvc— generates the Knative Service YAML dynamically -
run-kubectl— applies it to the cluster
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: deployment
spec:
params:
- name: imageUrl
type: string
- name: appName
type: string
steps:
- name: create-ksvc
image: bash:latest
command:
- /bin/sh
args:
- -c
- |
cat <<EOF > /workspace/knative-ksvc.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: $(params.appName)
namespace: default
labels:
application: $(params.appName)
tier: application
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/minScale: "1"
labels:
application: $(params.appName)
tier: application
spec:
containers:
- image: $(params.imageUrl)
imagePullPolicy: IfNotPresent
env:
- name: TARGET
value: "Tekton Sample"
ports:
- containerPort: 80
EOF
- name: run-kubectl
image: lachlanevenson/k8s-kubectl:v1.17.12
command: ['kubectl']
args:
- "apply"
- "-f"
- "/workspace/knative-ksvc.yaml"
Key points:
- The Knative Service manifest is generated dynamically using shell heredoc —
appNameandimageUrlare injected at runtime -
autoscaling.knative.dev/minScale: "1"keeps at least one instance running (no cold starts) - ✅ This Task is reusable — pass any
appNameandimageUrlto deploy any application
6. Pipeline: Wiring Everything Together
The Pipeline references all three Tasks and defines their execution order using runAfter:
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: helloworld-java-spring-pipeline
spec:
resources:
- name: git-source-p
type: git
- name: builtImage-p
type: image
params:
- name: application
type: string
tasks:
# Step 1: Generate image tag
- name: generate-image-url
taskRef:
name: generate-image-tag
resources:
outputs:
- name: builtImage
resource: builtImage-p
# Step 2: Build and push (depends on Step 1 for the image URL)
- name: maven-build-and-push
taskRef:
name: maven-build-and-push
runAfter:
- generate-image-url
resources:
inputs:
- name: git-source
resource: git-source-p
params:
- name: imageUrl
value: "$(tasks.generate-image-url.results.timestamp)"
# Step 3: Deploy (depends on Step 2)
- name: deployment
taskRef:
name: deployment
runAfter:
- maven-build-and-push
params:
- name: appName
value: $(params.application)
- name: imageUrl
value: "$(tasks.generate-image-url.results.timestamp)"
Execution flow:
generate-image-url
↓ (runAfter)
maven-build-and-push
↓ (runAfter)
deployment
Note that imageUrl is passed from Task 1's Result to both Task 2 and Task 3 — ensuring the same image tag is used throughout the pipeline.
7. PipelineRun: Triggering the Pipeline
Finally, create a PipelineRun to bind resources and kick off execution:
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: helloworld-java-spring-pipeline-run
spec:
pipelineRef:
name: helloworld-java-spring-pipeline
params:
- name: application
value: helloworld-java-spring
resources:
- name: git-source-p
resourceRef:
name: git-resource-helloworld-java-spring
- name: builtImage-p
resourceRef:
name: image-resource-helloworld-java-spring
serviceAccountName: docker-git-sa # must have Git pull + Docker push permissions
timeout: 0h10m0s
You can create this PipelineRun in three ways:
kubectl apply -f pipelinerun.yamltkn pipeline start helloworld-java-spring-pipeline- Via the Tekton Dashboard UI
8. Full Pipeline Architecture
PipelineRun
│
├── Task: generate-image-tag
│ └── Step: get-timestamp → writes image URL to Result
│
├── Task: maven-build-and-push (receives imageUrl from Result)
│ ├── Step: maven-compile → builds JAR, uses PVC cache
│ └── Step: build-and-push → Kaniko builds & pushes image
│
└── Task: deployment (receives imageUrl from Result)
├── Step: create-ksvc → generates Knative Service YAML
└── Step: run-kubectl → applies to cluster
9. Summary
| Component | Purpose |
|---|---|
PVC (maven-repo-local) |
Caches Maven dependencies across runs |
| PipelineResource (Git) | Defines source code input |
| PipelineResource (Image) | Defines image registry output |
| Task: generate-image-tag | Auto-generates timestamp-based image tag |
| Task: maven-build-and-push | Compiles Java code and pushes image via Kaniko |
| Task: deployment | Deploys app as a Knative Service |
| Pipeline | Wires Tasks together with runAfter ordering |
| PipelineRun | Instantiates the Pipeline with real resources |
This pipeline covers the full CI/CD loop. From here, you can extend it with:
- SonarQube Task for code quality analysis
- Trivy Task for container image vulnerability scanning
- Tekton Triggers to fire the pipeline automatically on every PR merge
Follow the series — next up we explore Knative Serving, the serverless runtime that pairs with this Tekton pipeline for zero-config autoscaling deployments.
Top comments (0)