Skip to main content

How to use Tekton to set up a CI pipeline with OpenShift Pipelines

Learn how to create a complete continuous integration pipeline, from dev to production, with YAML examples of the steps required to make it happen.
Image
Silver pipelines

Photo by Sigmund on Unsplash

The Tekton project is a new Kubernetes- and container-native way to manage continuous integration/continuous delivery (CI/CD) systems. It's also the basis for OpenShift Pipelines. This article demonstrates a reference implementation for OpenShift Pipelines using a sample Quarkus project.

This article has two objectives:

  • To show you how to set up a CI pipeline with OpenShift Pipelines using Tekton.
  • To serve as a quickstart example for building your own CI pipeline.

That's a lot to cover, but you might be surprised at just how manageable it is.

The pipeline looks like this:

Image
CI pipeline
(Dapo Oloyede, CC BY-SA 4.0)

The pipeline's goals are to:

  • Clone a source code repository at the dev branch.
  • Run test cases.
  • Build an artifact for the image.
  • Use a Trivy task to scan build artifacts in parallel for vulnerabilities.
  • Build a tag image using the source code commit ID.
  • Use a Trivy task to scan the image locally in parallel for vulnerabilities.
  • Push the image to the image repository ccop-dev (artifactory.xxx.corp.xxx.ca:5073).
  • Use a Trivy task to scan the image in its remote image repository for vulnerabilities.
  • Update the Kubernetes repository under the path k8s/overlays/dev on the dev branch to point to the latest image in the Artifactory registry.

Prerequisites

To run this reference pipeline (and possibly your pipeline), you or your cluster administrator must install Red Hat OpenShift Pipelines. It may already be included in Installed Operators.

Image
CI Operators
(Dapo Oloyede, CC BY-SA 4.0)

You must also create a new OpenShift namespace called cop-pipeline. You can do this in the UI or from a terminal:

$ oc create namespace cop-pipeline

Next, create or access both the reference source code repository and the reference Kubernetes repository.

Write some YAML

Most tasks in Kubernetes can be defined in YAML, so the first step in building this pipeline is to write some setup YAML.

[ Try our interactive scenarios to learn Red Hat OpenShift at your own pace. ]

From your profile page in the source code repository, set up an Access Token with the necessary permissions (api, read_api, read_repository, write_repository), as this screenshot shows:

Image
Personal access token with permissions
(Dapo Oloyede, CC BY-SA 4.0)

Note that the same Access Token is used for the Kubernetes repository in the reference implementation. If you have different user profiles, you must set up multiple Access Tokens.

Set up a secret for the Access Tokens in the cop-pipeline OpenShift namespace and annotate the secrets. The tekton.dev/git-0: 'https://gitlab.xxx.corp.xxx.ca' line shows the annotation in the YAML file below:

apiVersion: v1
metadata:
    name: gitlab-token
    namespace: cop-pipeline
annotations:
    tekton.dev/git-0: 'https://gitlab.xxx.corp.xxx.ca'
data:
    password: xxxxxxxxxxxxxxxxxx=
    username: xxxxxx==
type: kubernetes.io/basic-auth

Add the secrets to the service account pipeline:

kind: ServiceAccount
apiVersion: v1
metadata:
    name: pipeline
    namespace: cop-pipeline
secrets:
    - name: pipeline-token-xxxxx
    - name: pipeline-dockercfg-xxxx
    - name: gitlab-token
    - name: artifactory-token
imagePullSecrets:
    - name: pipeline-dockercfg-xxxxx

Run a CI pipeline in a dev environment

You can manually run the pipeline, or you can create a webhook on GitLab or GitHub to trigger it:

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: ci-pipeline
  namespace: cop-pipeline
spec:
  params:
    - description: git url to clone
      name: git-source-url
      type: string
    - default: master
      description: 'git revision to checkout (branch, tag, sha, ref…)'
      name: git-source-revision
      type: string
    - description: image tag - truncated commit Id
      name: short-commit-id
      type: string
    - default: 'http://sonarqube-cicd-tools.apps.ocp-dev-ade.xxxdev.dev.xxx.ca'
      description: SonarQube url for static code analysis
      name: SONAR_URL
      type: string
    - default: cb98fc7b68ea37feb56d151e10b87d6dc596f4b2
      description: SonarQube authentication token for static code analysis
      name: SONAR_AUTH_TOKEN
      type: string
    - default: ./
      description: image path
      name: LOCAL_SCAN_PATH
      type: string
    - default: ./
      description: image path
      name: LOCAL_IMAGE_SCAN_PATH
      type: string
    - default: 'artifactory.xxx.corp.xxx.ca:5073/ccop-dev/quarkus-ref-image-dev'
      description: image path
      name: REMOTE_IMAGE_URL
      type: string
    - default: 'artifactory.xxx.corp.xxx.ca:5073/ccop-dev/quarkus-ref-image-dev'
      description: image path for security scanning
      name: SCAN_IMAGE_URL
      type: string
    - default: UNKNOWN
      description: vulnerability severity level
      name: SEVERITY_LEVELS
      type: string
    - default: 'https://gitlab.xxx.corp.xxx.ca/xx/xxxxx/tekton-pipeline.git'
      description: Kustomize git repo for CD
      name: KUSTOMIZE_GIT_URL
      type: string
    - default: k8s/overlays/dev
      description: Kustomize git repo context directory for CD
      name: KUSTOMIZE_GIT_CONTEXT_DIR
      type: string
    - default: dev
      description: Kustomize git repo branch
      name: KUSTOMIZE_GIT_BRANCH
      type: string
  tasks:
    - name: git-clone
      params:
        - name: url
          value: $(params.git-source-url)
        - name: revision
          value: $(params.git-source-revision)
        - name: sslVerify
          value: 'false'
        - name: noProxy
          value: 'true'
      taskRef:
        kind: ClusterTask
        name: git-clone
      workspaces:
        - name: output
          workspace: app-source
    - name: run-test-cases
      params:
        - name: GOALS
          value:
            - clean
            - test
      runAfter:
        - git-clone
      taskRef:
        kind: Task
        name: maven
      workspaces:
        - name: source
          workspace: app-source
        - name: maven-settings
          workspace: maven-settings
    - name: static-code-analysis
      params:
        - name: GOALS
          value:
            - 'sonar:sonar'
            - '-Dsonar.projectKey=ci-pipeline-ref-arc'
            - '-Dsonar.host.url=$(params.SONAR_URL)'
            - '-Dsonar.login=$(params.SONAR_AUTH_TOKEN)'
            - '-Dsonar.exclusions=**/*.java'
            - '-s  $(workspaces.maven-settings.path)/settings.xml'
      runAfter:
        - run-test-cases
      taskRef:
        kind: Task
        name: maven
      workspaces:
        - name: source
          workspace: app-source
        - name: maven-settings
          workspace: maven-settings
    - name: build-artifact
      params:
        - name: GOALS
          value:
            - '-DskipTests'
            - package
            - '-Dquarkus.native.container-build=true'
      runAfter:
        - static-code-analysis
      taskRef:
        kind: ClusterTask
        name: maven
      workspaces:
        - name: source
          workspace: app-source
        - name: maven-settings
          workspace: maven-settings
    - name: scan-build-artifact
      params:
        - name: SCAN_TYPE
          value: filesystem
        - name: SEVERITY_LEVELS
          value: $(params.SEVERITY_LEVELS)
        - name: SCAN_PATH_OR_IMAGE_URL
          value: $(params.LOCAL_SCAN_PATH)
      runAfter:
        - build-artifact
      taskRef:
        kind: Task
        name: trivy-scan
      workspaces:
        - name: local-image-repo
          workspace: app-source
    - name: build-image
      params:
        - name: IMAGE
          value: $(params.REMOTE_IMAGE_URL)
        - name: TLSVERIFY
          value: 'false'
      runAfter:
        - build-artifact
      taskRef:
        kind: Task
        name: buildah-build
      workspaces:
        - name: source
          workspace: app-source
        - name: varlibcontainers
          workspace: shared-image-repo
    - name: scan-local-image
      params:
        - name: SCAN_TYPE
          value: filesystem
        - name: SEVERITY_LEVELS
          value: $(params.SEVERITY_LEVELS)
        - name: SCAN_PATH_OR_IMAGE_URL
          value: $(params.LOCAL_IMAGE_SCAN_PATH)
      runAfter:
        - build-image
      taskRef:
        kind: Task
        name: trivy-scan
      workspaces:
        - name: local-image-repo
          workspace: shared-image-repo
    - name: push-image
      params:
        - name: IMAGE
          value: $(params.REMOTE_IMAGE_URL)
        - name: IMAGE_TAG
          value: $(params.short-commit-id)
        - name: TLSVERIFY
          value: 'false'
      runAfter:
        - build-image
      taskRef:
        kind: Task
        name: buildah-push
      workspaces:
        - name: source
          workspace: app-source
        - name: varlibcontainers
          workspace: shared-image-repo
    - name: scan-remote-image
      params:
        - name: SCAN_TYPE
          value: image
        - name: SEVERITY_LEVELS
          value: $(params.SEVERITY_LEVELS)
        - name: SCAN_PATH_OR_IMAGE_URL
          value: '$(params.SCAN_IMAGE_URL):$(params.short-commit-id)'
        - name: IGNORE_UNFIXED
          value: 'true'
      runAfter:
        - push-image
      taskRef:
        kind: Task
        name: trivy-scan
      workspaces:
        - name: local-image-repo
          workspace: shared-image-repo
    - name: update-kustomize-repo
      params:
        - name: gitRepositoryUrl
          value: $(params.KUSTOMIZE_GIT_URL)
        - name: gitRepositoryRevision
          value: $(params.KUSTOMIZE_GIT_BRANCH)
        - name: gitPath
          value: $(params.KUSTOMIZE_GIT_CONTEXT_DIR)
        - name: imageTag
          value: $(params.short-commit-id)
        - name: verbose
          value: 'true'
      runAfter:
        - scan-remote-image
      taskRef:
        kind: Task
        name: update-kustomize-repo
      workspaces:
        - name: repository
          workspace: kustomize-repo
  workspaces:
    - name: app-source
    - name: maven-settings
    - name: shared-image-repo
    - name: kustomize-repo

A successful pipeline run on the dev environment returns something like this:

Image
Successful pipeline run on the dev environment
(Dapo Oloyede, CC BY-SA 4.0)

A failed pipeline, on the other hand, returns this:

Image
Failed pipeline run
(Dapo Oloyede, CC BY-SA 4.0)

Create webhooks to trigger the pipeline run

Next, set up the following triggers on OpenShift.

First, create a trigger template using this YAML code:

apiVersion: triggers.tekton.dev/v1alpha1
kind: TriggerTemplate
metadata:
  name: ci-pipeline-template
  namespace: cop-pipeline
spec:
  params:
    - name: git-repo-url
    - name: git-revision
  resourcetemplates:
    - apiVersion: tekton.dev/v1beta1
      kind: PipelineRun
      metadata:
        generateName: ci-pipeline-
      spec:
        params:
          - name: git-source-url
            value: $(tt.params.git-repo-url)
          - name: git-source-revision
            value: $(tt.params.git-revision)
          - name: SONAR_URL
            value: 'http://sonarqube:9000'
          - name: SONAR_AUTH_TOKEN
            value: xxxxxxxxxxxxxxxxxxxxx
          - name: LOCAL_SCAN_PATH
            value: ./
          - name: LOCAL_IMAGE_SCAN_PATH
            value: ./
          - name: REMOTE_IMAGE_URL
            value: >-
              image-registry.openshift-image-registry.svc:5000/cop-pipeline/xxx-app-image
          - name: SEVERITY_LEVELS
            value: CRITICAL
          - name: KUSTOMIZE_GIT_URL
            value: 'https://gitlab.xxx.corp.xxx.ca/xx/xxxxx/tekton-pipeline.git'
          - name: KUSTOMIZE_GIT_CONTEXT_DIR
            value: k8s/overlays/dev
        pipelineRef:
          name: ci-pipeline
        workspaces:
          - name: app-source
            persistentVolumeClaim:
              claimName: workspace-pvc2
          - name: maven-settings
            persistentVolumeClaim:
              claimName: maven-settings-pvc
          - name: shared-image-repo
            persistentVolumeClaim:
              claimName: workspace-pvc2
          - emptyDir: {}
            name: kustomize-repo

Next, create a trigger binding using this YAML:

apiVersion: triggers.tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
  name: ci-pipeline-binding
  namespace: cop-pipeline
spec:
  params:
    - name: git-repo-url
      value: $(body.repository.git_http_url)
    - name: git-revision
      value: $(body.after)

Then create an event listener:

apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
  name: ci-pipeline
  namespace: cop-pipeline
spec:
  namespaceSelector: {}
  podTemplate: {}
  resources: {}
  serviceAccountName: pipeline
  triggers:
    - bindings:
        - kind: TriggerBinding
          ref: ci-pipeline-binding
      name: ci-pipeline
      template:
        ref: ci-pipeline-template

In the GitLab source code repository, go to Settings/webhooks and set up a webhook for the repo to initiate a pipeline run after a successful commit.

Image
Project hooks dialog
(Dapo Oloyede, CC BY-SA 4.0)

Promote code to staging

Here's the YAML to promote your code to the staging environment:

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: uat-cd-pipeline
  namespace: cop-pipeline
spec:
  params:
    - description: Git commitId of the change to deploy
      name: IMAGE_TAG
      type: string
    - default: >-
        image-registry.openshift-image-registry.svc:xxxx/cop-pipeline/dapo-app-image
      description: Source image url without tag
      name: SRC_IMAGE_URL
      type: string
    - default: 'image-registry.openshift-image-registry.svc:xxxx/ccop-dev/dapo-app-image-uat'
      description: Destination image url without tag
      name: DEST_IMAGE_URL
      type: string
    - default: UNKNOWN
      description: Image Scan severity levels
      name: SEVERITY_LEVELS
      type: string
    - default: 'https://gitlab.xxx.xxxx.xxx.ca/tekton-pipeline.git'
      description: Kustomize git repo for CD
      name: KUSTOMIZE_GIT_URL
      type: string
    - default: k8s/overlays/uat
      description: Kustomize git repo context directory for CD
      name: KUSTOMIZE_GIT_CONTEXT_DIR
      type: string
    - default: uat
      description: Kustomize git repo branch
      name: KUSTOMIZE_GIT_BRANCH
      type: string
  tasks:
    - name: skopeo-copy
      params:
        - name: srcImageURL
          value: 'docker://$(params.SRC_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: destImageURL
          value: 'docker://$(params.DEST_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: srcTLSverify
          value: 'false'
        - name: destTLSverify
          value: 'false'
      taskRef:
        kind: ClusterTask
        name: skopeo-copy
      workspaces:
        - name: images-url
          workspace: images-url
    - name: trivy-scan
      params:
        - name: NO_PROXY
          value: 'localhost,127.0.0.1'
        - name: HTTP_PROXY
          value: ''
        - name: INSECURE_REGISTRY
          value: 'false'
        - name: SEVERITY_LEVELS
          value: $(params.SEVERITY_LEVELS)
        - name: SCAN_TYPE
          value: image
        - name: TRIVY_IMAGE
          value: >-
            image-registry.openshift-image-registry.svc:5000/cop-pipeline/trivy-image:v0.18.3
        - name: SCAN_PATH_OR_IMAGE_URL
          value: '$(params.DEST_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: IGNORE_UNFIXED
          value: 'false'
      runAfter:
        - skopeo-copy
      taskRef:
        kind: Task
        name: trivy-scan
      workspaces:
        - name: local-image-repo
          workspace: image-repo
    - name: update-kustomize-repo
      params:
        - name: gitRepositoryUrl
          value: $(params.KUSTOMIZE_GIT_URL)
        - name: gitRepositoryRevision
          value: $(params.KUSTOMIZE_GIT_BRANCH)
        - name: gitPath
          value: $(params.KUSTOMIZE_GIT_CONTEXT_DIR)
        - name: fileName
          value: deployment-patches.yaml
        - name: imageTag
          value: $(params.IMAGE_TAG)
        - name: verbose
          value: 'true'
      runAfter:
        - trivy-scan
      taskRef:
        kind: Task
        name: update-kustomize-repo
      workspaces:
        - name: repository
          workspace: kustomize-repo
  workspaces:
    - name: images-url
    - name: image-repo
    - name: kustomize-repo

The pipeline looks like this:

Image
UAT production pipeline
(Dapo Oloyede, CC BY-SA 4.0)

The pipeline performs a Skopeo copy from the dev image repository to the staging environment's image repository. It then scans the image in the remote image repository for vulnerabilities using a Trivy task.

[ Free eBook: Getting GitOps: A practical platform with OpenShift, Argo CD, and Tekton. ]

The pipeline also updates the repository under the path k8s/overlays/uat in the uat branch to point to the latest image in the Artifactory registry.

Promote to production

Refer to the source YAML below to promote your code to production:

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: prod-cd-pipeline
  namespace: cop-pipeline
spec:
  params:
    - description: Git commitId of the change to deploy
      name: IMAGE_TAG
      type: string
    - default: >-
        image-registry.openshift-image-registry.svc:xxxx/cop-pipeline/dapo-app-image
      description: Source image url without tag
      name: SRC_IMAGE_URL
      type: string
    - default: 'image-registry.openshift-image-registry.svc:xxxx/ccop-dev/dapo-app-image-prod'
      description: Destination image url without tag
      name: DEST_IMAGE_URL
      type: string
    - default: UNKNOWN
      description: Image Scan severity levels
      name: SEVERITY_LEVELS
      type: string
    - default: 'https://gitlab.xxx.xxxx.xxx.ca/tekton-pipeline.git'
      description: Kustomize git repo for CD
      name: KUSTOMIZE_GIT_URL
      type: string
    - default: k8s/overlays/prod
      description: Kustomize git repo context directory for CD
      name: KUSTOMIZE_GIT_CONTEXT_DIR
      type: string
    - default: prod
      description: Kustomize git repo branch
      name: KUSTOMIZE_GIT_BRANCH
      type: string
  tasks:
    - name: skopeo-copy
      params:
        - name: srcImageURL
          value: 'docker://$(params.SRC_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: destImageURL
          value: 'docker://$(params.DEST_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: srcTLSverify
          value: 'false'
        - name: destTLSverify
          value: 'false'
      taskRef:
        kind: ClusterTask
        name: skopeo-copy
      workspaces:
        - name: images-url
          workspace: images-url
    - name: trivy-scan
      params:
        - name: NO_PROXY
          value: 'localhost,127.0.0.1'
        - name: HTTP_PROXY
          value: ''
        - name: INSECURE_REGISTRY
          value: 'false'
        - name: SEVERITY_LEVELS
          value: $(params.SEVERITY_LEVELS)
        - name: SCAN_TYPE
          value: image
        - name: TRIVY_IMAGE
          value: >-
            image-registry.openshift-image-registry.svc:5000/cop-pipeline/trivy-image:v0.18.3
        - name: SCAN_PATH_OR_IMAGE_URL
          value: '$(params.DEST_IMAGE_URL):$(params.IMAGE_TAG)'
        - name: IGNORE_UNFIXED
          value: 'false'
      runAfter:
        - skopeo-copy
      taskRef:
        kind: Task
        name: trivy-scan
      workspaces:
        - name: local-image-repo
          workspace: image-repo
    - name: update-kustomize-repo
      params:
        - name: gitRepositoryUrl
          value: $(params.KUSTOMIZE_GIT_URL)
        - name: gitRepositoryRevision
          value: $(params.KUSTOMIZE_GIT_BRANCH)
        - name: gitPath
          value: $(params.KUSTOMIZE_GIT_CONTEXT_DIR)
        - name: fileName
          value: deployment-patches.yaml
        - name: imageTag
          value: $(params.IMAGE_TAG)
        - name: verbose
          value: 'true'
      runAfter:
        - trivy-scan
      taskRef:
        kind: Task
        name: update-kustomize-repo
      workspaces:
        - name: repository
          workspace: kustomize-repo
  workspaces:
    - name: images-url
    - name: image-repo
    - name: kustomize-repo

The pipeline looks like this:

Image
UAT production pipeline
(Dapo Oloyede, CC BY-SA 4.0)

Skopeo copies this pipeline from the dev image repository to the production image repository and then scans the image in the remote image repository for vulnerabilities using a Trivy task.

It also updates the repository under the k8s/overlays/prod path in the master branch to point to the latest image in the Artifactory image repository.

A complete pipeline

Now you know the components involved in creating a complete CI pipeline, from dev to production, and you have YAML examples of the steps required to make it happen. Refer to the reference repo as you develop a pipeline for your own code. The repositories and environments will differ, but the principles are the same.

In my follow-up article, I'll demonstrate how to create a CD pipeline with ArgoCD.

What to read next

Topics:   Kubernetes   OpenShift   DevOps  
Author’s photo

Dapo Oloyede

Dapo is a Senior Application Development Consultant at Red Hat and a Red Hat Certified Architect in Enterprise Applications (RHCA). He has over 15 years of experience in enterprise application development; and also has a strong passion for digital transformation in organizations. In his spare tim More about me

Author’s photo

Olu Oteniya

Olu is a Senior Architect at Red Hat, working with various customers in the last few years to formulate their digital transformation strategy to respond to the constantly changing business environments. More about me

Related Content

OUR BEST CONTENT, DELIVERED TO YOUR INBOX