DEV Community

James Lee
James Lee

Posted on

Tekton in Practice: Building a Java CI/CD Pipeline from Scratch

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:

  1. Auto-generate image tag (timestamp-based)
  2. Maven build + Kaniko image push
  3. 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. maven-compile — builds the Java project, skipping tests for speed
  2. 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
Enter fullscreen mode Exit fullscreen mode

Key points:

  • maven-compile mounts the PVC at /root/.m2 — Maven's local cache directory
  • build-and-push uses 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:

  1. create-ksvc — generates the Knative Service YAML dynamically
  2. 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"
Enter fullscreen mode Exit fullscreen mode

Key points:

  • The Knative Service manifest is generated dynamically using shell heredoc — appName and imageUrl are injected at runtime
  • autoscaling.knative.dev/minScale: "1" keeps at least one instance running (no cold starts)
  • ✅ This Task is reusable — pass any appName and imageUrl to 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)"
Enter fullscreen mode Exit fullscreen mode

Execution flow:

generate-image-url
       ↓ (runAfter)
maven-build-and-push
       ↓ (runAfter)
deployment
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can create this PipelineRun in three ways:

  • kubectl apply -f pipelinerun.yaml
  • tkn 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
Enter fullscreen mode Exit fullscreen mode

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)