In this second part of the Tekton CICD example project, we’ll delve into the design and implementation of the CI/CD pipeline. We’ll walk through an example project, demonstrating how to structure and orchestrate the various stages of the pipeline to streamline the software delivery lifecycle.
Our example project focuses on a simple application built with Go, a popular programming language for building scalable and efficient software. We’ll explore how Tekton pipelines can be leveraged to automate tasks such as linting, uni- and integration testing, building container images, packaging helm charts, and deploying to desired environments.
1 Pipeline overview
Let us have a look at a common pattern of a pipeline strategy you can face.
Feature branch
When committing changes to a feature branch, the pipeline initiates with the following stages:
1. Notify GitHub: This very first step will notify github via status-api the beginning of a build.
2. Lint and test: The code changes are linted and subjected to unit or integration tests.
3. Go build: If the linting and tests pass successfully, the code is built using the Go compiler.
4. Notify GitHub: This last step will notify github via status-api the status of the build.
Main branch
Commits to the main branch initiate the following stages:
1. Notify GitHub: This very first step will notify github via status-api the beginning of a build.
2. Lint and test: The code changes are linted and subjected to unit or integration tests.
3. Go build: If the linting and tests pass successfully, the code is built using the Go compiler.
4. Container image build: The built code is containerized into a container image tagged as the latest version and pushed to a container registry.
5. Helm chart package: The helm chart is packaged and published as latest to a central location.
6. Deploy to dev stage: This step is used to deploy the latest version into the dev namespace on the current kubernetes cluster.
7. Notify GitHub: This last step will notify github via status-api the status of the build.
Git tag created
When a Git tag is created, indicating a release version, the following stages are executed:
1. Notify GitHub: This very first step will notify github via status-api the beginning of a build
2. Lint and test: The code changes are linted and subjected to unit or integration tests.
3. Go build: If the linting and tests pass successfully, the code is built using the Go compiler.
4. Container image build: The built code is containerized into a container image tagged as the latest version and pushed to a container registry.
5. Helm chart package: The helm chart versioned based on the created git-tag is packaged and published to a central location.
6. Deploy to qa stage: This step is used to deploy the git-tag version into the qa namespace on the current kubernetes cluster.
7. Notify GitHub: This last step will notify github via status-api the status of the build.
2 Pipeline implementation
Now that we have designed our CI/CD pipeline using Tekton, let’s dive into its implementation details. Our pipeline is structured to automate the build, test, and deployment processes for a Go-based application. Below, we’ll explore each component and configuration file involved in our pipeline setup.
Directory structure
. |-- pipelines | `-- 7.02-go-pipeline.yaml |-- rbac | |-- 7.02-rbac-deployer.yaml | `-- 7.02-rbac-trigger.yaml |-- secrets | |-- 7.02-github-interceptor-secret.yaml | |-- 7.02-github-repo-secret.yaml | |-- 7.02-github-status-api-secret.yaml | `-- 7.02-quay-secret.yaml |-- tasks | `-- 7.02-helm-build-push.yaml `-- trigger |-- 7.02-eventlistener.yaml |-- 7.02-tb.yaml |-- 7.02-trigger.yaml `-- 7.02-tt.yaml
./pipelines: Contains the Tekton pipeline configuration file.
./rbac: Contains the rbac for the trigger and the deployer service-account
./secrets: Includes secrets such as for quay & github.
./tasks: Holds self-maintained task definitions used within the pipeline.
./trigger: Contains configuration files for triggering pipeline execution.
3 Pipeline definition
Our pipeline configuration is defined in the 7.02-go-pipeline.yaml
file. This file outlines the workflow of our CI/CD pipeline.
7.02-go-pipeline.yaml
apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: 7-02-go-pipeline namespace: default spec: workspaces: - name: shared-workspace - name: helm-workspace - name: dockerconfig-ws params: - name: git-url description: Git Repo to checkout the desired git repository type: string - name: git-revision description: Revision to be used from repo of the code for building and testing type: string default: main - name: git-repo-full-name description: Used to leverage git status api type: string - name: is-main-branch description: Flag to be used for conditional pipeline executing (CI CD up to dev namespace) type: string default: false - name: is-git-tag description: Flag to be used for conditional pipeline executing (CI CD up to qa namespace) type: string default: false - name: image-repository description: Image registry where images are going to be pushed type: string default: quay.io/christoph_linse - name: app-name description: App name to be used for container-image and helm-chart build type: string - name: image-tag description: Container-image tag to be used type: string default: latest - name: chart-tag description: Default helm-chart tag to be used type: string default: 1.0.0 - name: golang-version description: Version of golang to be used type: string default: "1.22" tasks: - name: notify-github taskRef: name: github-set-status params: - name: REPO_FULL_NAME value: "$(params.git-repo-full-name)" - name: SHA value: "$(params.git-revision)" - name: DESCRIPTION value: "Build has started" - name: STATE value: pending - name: TARGET_URL value: "http://localhost:9097/#/pipelineruns" - name: fetch-source taskRef: name: git-clone params: - name: url value: $(params.git-url) - name: deleteExisting value: "true" - name: revision value: $(params.git-revision) workspaces: - name: output workspace: shared-workspace - name: test-code taskRef: name: golang-test params: - name: package value: "$(params.app-name)" - name: version value: "$(params.golang-version)" workspaces: - name: source workspace: shared-workspace runAfter: - fetch-source - name: lint-code taskRef: name: golangci-lint params: - name: package value: "$(params.app-name)" - name: flags value: --verbose - name: version value: "v$(params.golang-version)" workspaces: - name: source workspace: shared-workspace runAfter: - fetch-source - name: build-image taskRef: name: buildah params: - name: IMAGE value: $(params.image-repository)/$(params.app-name):$(params.image-tag) workspaces: - name: source workspace: shared-workspace - name: dockerconfig workspace: dockerconfig-ws runAfter: - lint-code - test-code when: - input: "true" operator: in values: [ "$(params.is-main-branch)", "$(params.is-git-tag)" ] - name: build-helm taskRef: name: 7-02-helm-build-push params: - name: HELM_CHART_NAME value: "$(params.app-name)" - name: HELM_CHART_TAG value: "$(params.chart-tag)" workspaces: - name: source workspace: shared-workspace - name: helm-repo workspace: helm-workspace runAfter: - build-image when: - input: "true" operator: in values: [ "$(params.is-main-branch)", "$(params.is-git-tag)" ] - name: deploy-dev taskRef: name: helm-upgrade-from-repo params: - name: helm_repo value: https://c-linse.github.io/tekton-course-charts - name: chart_name value: "tekton-course-charts/$(params.app-name)" - name: release_version value: "$(params.chart-tag)" - name: release_name value: "$(params.app-name)" - name: release_namespace value: dev runAfter: - build-helm when: - input: "true" operator: in values: [ "$(params.is-main-branch)" ] - name: deploy-qa taskRef: name: helm-upgrade-from-repo params: - name: helm_repo value: https://c-linse.github.io/tekton-course-charts - name: chart_name value: "tekton-course-charts/$(params.app-name)" - name: release_version value: "$(params.chart-tag)" - name: release_name value: "$(params.app-name)" - name: release_namespace value: qa runAfter: - build-helm when: - input: "true" operator: in values: [ "$(params.is-git-tag)" ] finally: - name: notify-git-state-success taskRef: name: github-set-status when: - input: "$(tasks.status)" operator: notin values: [ "Failed" ] params: - name: REPO_FULL_NAME value: "$(params.git-repo-full-name)" - name: SHA value: "$(params.git-revision)" - name: DESCRIPTION value: "Build has been successful" - name: STATE value: success - name: TARGET_URL value: "http://localhost:9097/#/pipelineruns"
Key features of go-pipeline:
- GitHub repository integration: The pipeline fetches source code from a git repository, enabling seamless integration with version control systems such as GitHub.
- Linting and testing: Utilizes linting to ensure code quality by identifying linting errors and potential issues in the codebase and performs testing like unit and integration tests.
- Container image building: Builds container images containing the Go application, facilitating containerization for deployment across various environments.
- Helm chart packaging: Packages the containerized applications manifests into a helm chart, simplifying the deployment process in kubernetes environments.
- Deployment dev/qa: Deploys into the same cluster into the dev or qa namespace depending on the git revision (main branch or git tag)
Customizable parameters:
- git-url: Git repo to checkout the desired code base
- git-revision: Revision to be used from repo of the code
- git-repo-full-name: Used to leverage git status api
- is-main-branch: Flag to be used for conditional pipeline executing (CI CD up to dev namespace)
- is-git-tag: Flag to be used for conditional pipeline executing (CI CD up to qa namespace)
- image-repository: Image registry where images are going to be pushed to
- app-name: App name to be used for container-image and helm-chart build
- image-tag: Container-image tag to be used
- chart-tag: Default helm-chart tag to be used
- golang-version: Version of golang to be used
Conditional execution:
- The pipeline includes conditional logic to differentiate between feature branch commits and main branch commits. Certain tasks, such as container image building and helm chart packaging, are executed only for commits to the main branch and on git tag creation, optimizing resource usage and ensuring efficient pipeline execution.
4 Task installation
To install Tekton tasks on the GKE cluster, let’s have a look at the provided script. These task are beeing used by the tekton pipeline we have just defined.
Task Installation
tkn hub install task git-clone --version 0.9 --namespace default tkn hub install task golangci-lint --version 0.2 --namespace default tkn hub install task golang-test --version 0.2 --namespace default tkn hub install task buildah --version 0.7 --namespace default tkn hub install task helm-upgrade-from-repo --version 0.2 --namespace default tkn hub install task github-set-status --version 0.4 --namespace default
5 Custom helm task
The helm-build-push task is a custom Tekton task designed to lint and package a Helm chart into a tar.gz file and push it to a
web server. This task streamlines the process of packaging and publishing helm charts, facilitating the deployment of
applications in Kubernetes environments.
Parameters
HELM_CHART_NAME: Defines the name of the helm chart to be packaged and pushed.
HELM_CHART_TAG: Specifies the tag to be used for the helm chart version.
HELM_TARGET_ARTIFACTORY: Specifies the server URL to which the helm chart will be pushed.
Steps
1. helm-lint: Lint the Helm chart to dectect potential linting issues
2. helm-package: Packages the helm chart into a tar.gz file using the specified chart name and version.
3. helm-publish: Clones the target artifact repository, copies the packaged chart, updates the repository index, commits the changes, and pushes them to the remote repository.
The task looks like the following:
7.02-helm-build-push.yaml
apiVersion: tekton.dev/v1 kind: Task metadata: name: 7-02-helm-build-push spec: description: This task run helm lint and packages a helm chart to tar.gz and pushes it into a webserver params: - name: HELM_CHART_NAME description: Chart name to use for packaging and pushing type: string - name: HELM_CHART_TAG description: Tag to be used for helm build and push type: string - name: HELM_TARGET_ARTIFACTORY description: Server to be used for publishing helm package type: string default: https://github.com/c-linse/tekton-course-charts.git workspaces: - name: source - name: helm-repo steps: - name: helm-lint image: dtzar/helm-kubectl:3.14.1 workingDir: $(workspaces.source.path) script: | #!/bin/bash set -e cd helm helm lint . - name: helm-package image: dtzar/helm-kubectl:3.14.1 workingDir: $(workspaces.source.path) script: | #!/bin/bash set -e cd helm helm package . \ --version "$(params.HELM_CHART_TAG)" \ --app-version "$(params.HELM_CHART_TAG)" \ -d "$(workspaces.source.path)/$(params.HELM_CHART_NAME)" - name: helm-publish image: dtzar/helm-kubectl:3.14.1 workingDir: $(workspaces.helm-repo.path) script: | #!/bin/bash set -e git clone $(params.HELM_TARGET_ARTIFACTORY) . git config user.name "c-linse" git config user.email "christophlinse@googlemail.com" mkdir -p "$(params.HELM_CHART_NAME)" cp -R \ "$(workspaces.source.path)/$(params.HELM_CHART_NAME)/$(params.HELM_CHART_NAME)-$(params.HELM_CHART_TAG).tgz" \ "$(params.HELM_CHART_NAME)/" helm repo index . git add index.yaml "$(params.HELM_CHART_NAME)" git commit -m "$(params.HELM_CHART_NAME)-$(params.HELM_CHART_TAG)" git push origin main
6 Secrets
In our pipeline setup, we utilize a couple of secrets to securely manage sensitive information required for various operations. These secrets can be created and maintained in GitHub on project and organization level and of course on quay.io for the container registry secret. These secrets have to be configured and applied to kubernetes secrets as mention in the following:
GitHub repo secret
This secret contains the authentication credentials required to access the GitHub repository hosting the application code. This secret will be used by the git clone task from the TektonHub.
7.02-github-repo-secret.yaml
apiVersion: v1 kind: Secret metadata: name: 7-02-git-repo-secret annotations: tekton.dev/git-0: https://github.com type: kubernetes.io/basic-auth stringData: username: "c-linse" password: ""
GitHub status api secret
This secret holds a GitHub token used for authentication when interacting with GitHub status api. It will be used to update the state of the git-revision the pipeline executes.
7.02-github-status-api-secret.yaml
apiVersion: v1 kind: Secret metadata: name: github namespace: default type: Opaque data: token: ""
GitHub interceptor secret
This secret holds a GitHub token used for authentication when interacting with GitHub repositories. It will be used by the tekton trigger interceptor to ensure that only valid webhooks will instantiate the pipeline.
7.02-github-interceptor-secret.yaml
apiVersion: v1 kind: Secret metadata: name: 7-02-github-interceptor-secret type: Opaque stringData: token: ""
Quay registry secret
This secret stores the authentication credentials necessary for pushing container images to the https://quay.io container registry. It ensures secure access to the registry, facilitating image publishing and distribution. This secret will be used by the buildah task from the tekton hub.
7.02-quay-secret.yaml
apiVersion: v1 kind: Secret metadata: name: 7-02-quay-secret stringData: config.json: | { "auths": { "quay.io/christoph_linse": { "auth": "" } } }
7 Trigger definition
Eventlistener
The eventlistener resource 7-02-github-el will be additionally exposed by a loadbalancer. That instructs GCP to
expose the eventlistener pod with a public ip address which we are able to reach and more important GitHub can leverage for sending webhooks.
7.02-eventlistener.yaml
apiVersion: triggers.tekton.dev/v1beta1 kind: EventListener metadata: name: 7-02-github-el spec: triggers: - triggerRef: 7-02-github-trigger serviceAccountName: 7-02-tekton-triggers-sa --- apiVersion: v1 kind: Service metadata: name: lb-tekton-event-listener-service spec: selector: eventlistener: 7-02-github-el ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer
TriggerBinding
The TriggerBinding resource named 7-02-github-tb defines parameters that extract information from GitHub webhook
payloads. It captures details like the repository url, revision, image tag, whether it’s a git-tag and more.
7.02-tb.yaml
apiVersion: triggers.tekton.dev/v1beta1 kind: TriggerBinding metadata: name: 7-02-github-tb namespace: default spec: params: - name: git-repository-url value: $(body.repository.clone_url) - name: git-repo-full-name value: $(body.repository.full_name) - name: git-revision value: $(body.head_commit.id) - name: app-name value: $(body.repository.name) - name: image-tag value: $(extensions.image-tag) - name: chart-tag value: $(extensions.chart-tag) - name: is-main-branch value: $(extensions.is-main-branch) - name: is-git-tag value: $(extensions.is-git-tag)
TriggerTemplate
The TriggerTemplate resource named 7-02-github-tt defines parameters and resource templates for pipeline runs. It
extracts parameters from the TriggerBinding and configures workspace volumes for pipeline execution, ensuring access
to required resources like Git repositories and Docker configuration secrets.
7.02-tt.yaml
apiVersion: triggers.tekton.dev/v1beta1 kind: TriggerTemplate metadata: name: 7-02-github-tt namespace: default spec: params: - name: git-repository-url - name: git-revision - name: git-repo-full-name - name: image-tag - name: chart-tag - name: app-name - name: is-main-branch - name: is-git-tag resourcetemplates: - apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: generateName: go-pipeline-run- namespace: default spec: pipelineRef: name: 7-02-go-pipeline podTemplate: securityContext: fsGroup: 65532 # to make the volumes accessible by the non-root user, we need to either configure the permissions manually or set the fsGroup taskRunSpecs: - pipelineTaskName: deploy-dev taskServiceAccountName: 7-02-deployer-sa - pipelineTaskName: deploy-qa taskServiceAccountName: 7-02-deployer-sa params: - name: git-url value: $(tt.params.git-repository-url) - name: git-revision value: $(tt.params.git-revision) - name: git-repo-full-name value: $(tt.params.git-repo-full-name) - name: app-name value: $(tt.params.app-name) - name: image-tag value: $(tt.params.image-tag) - name: chart-tag value: $(tt.params.chart-tag) - name: is-main-branch value: $(tt.params.is-main-branch) - name: is-git-tag value: $(tt.params.is-git-tag) workspaces: - name: shared-workspace volumeClaimTemplate: spec: accessModes: - ReadWriteOnce # access mode may affect how you can use this volume in parallel tasks resources: requests: storage: 10Mi - name: dockerconfig-ws secret: secretName: 7-02-quay-secret - name: helm-workspace emptyDir: medium: Memory
Trigger
The Trigger resource 7-02-github-trigger binds to the 7-02-github-tb TriggerBinding and references the
7-02-github-tt TriggerTemplate. It intercepts GitHub webhook events using interceptors, modifies parameters using CEL
expressions, and triggers pipeline-runs based on the defined conditions.
To have a detail look the following yaml must be seen:
7.02-trigger.yaml
apiVersion: triggers.tekton.dev/v1beta1 kind: Trigger metadata: name: 07-github-trigger namespace: default spec: bindings: - ref: 07-github-tb template: ref: 07-github-tt interceptors: - ref: name: "github" kind: ClusterInterceptor apiVersion: triggers.tekton.dev params: - name: "secretRef" value: secretName: 07-github-interceptor-secret secretKey: token - ref: name: "cel" params: - name: overlays value: - key: is-main-branch expression: body.ref.matches('refs/heads/main') - key: is-git-tag expression: body.ref.matches('refs/tags/.*') - key: image-tag expression: body.ref.split('/')[2].replace('main','latest') - key: chart-tag expression: body.ref.split('/')[2].replace('main','1.0.0')
8 RBAC
Rbac is beeing used to enable the service-account 7-02-tekton-triggers-sa to create tekton resources and the service-account 7-02-deployer-sa to be able to deploy to certain stages.
Setup Namespaces
Setup dev & qa namespace
$ kubectl create namespace dev $ kubectl create namespace qa
RBAC-Trigger
7.02-rbac-trigger.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: 7-02-tekton-triggers-sa namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: 7-02-triggers-eventlistener-binding namespace: default subjects: - kind: ServiceAccount name: 7-02-tekton-triggers-sa namespace: default roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: tekton-triggers-eventlistener-roles --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: 7-02-triggers-eventlistener-clusterbinding subjects: - kind: ServiceAccount name: 7-02-tekton-triggers-sa namespace: default roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: tekton-triggers-eventlistener-clusterroles
RBAC-Deployer
7.02-rbac-deployer.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: 7-02-deployer-sa namespace: default --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: dev name: 7-02-deployer-role-dev rules: - apiGroups: [""] resources: ["secrets", "configmaps", "deployments", "serviceaccounts","services"] verbs: ["get", "list", "create", "update", "delete", "patch"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "list", "create", "update", "delete", "patch"] --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: qa name: 7-02-deployer-role-qa rules: - apiGroups: [""] resources: ["secrets", "configmaps", "deployments", "serviceaccounts", "services"] verbs: ["get", "list", "create", "update", "delete", "patch"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "list", "create", "update", "delete", "patch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: 7-02-deployer-dev-rolebinding namespace: dev subjects: - kind: ServiceAccount name: 7-02-deployer-sa namespace: default roleRef: kind: Role name: 7-02-deployer-role-dev apiGroup: rbac.authorization.k8s.io --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: 7-02-deployer-qa-rolebinding namespace: qa subjects: - kind: ServiceAccount name: 7-02-deployer-sa namespace: default roleRef: kind: Role name: 7-02-deployer-role-qa apiGroup: rbac.authorization.k8s.io
9 References & links
Checkout Github for code and the Tekton Udemy course to understand and get even more content!
Hi.
I’ve been directed to this as additional content in the Udemy «tekton-the-quick-start» guide.
I could use a bit more direction
1: on how to configure the secrets files, whether I need to setup my own Quay account etc. I assume I need to clone the repo to my own github account to setup the webhook under part 3.
2: How to implement – with secrets correctly confirect can I just kubectl apply -f the whole 07_TektonGCP directory, if sure the current context is my GCP project having the k8s cluster set up?
Thanks
Hi Richard,
About your first question: You need to create an quay.io account in order to store container images in your quay repositories.
In order to derive the «quay» secret you have multiple options.
One option is for example using «docker/podman login»: podman login quay.io/
Afterwards you can get the encoded password from the following locations:
When using Podman -> cat ~/.config/containers/auth.json
When using Docker -> ~/.docker/config.json
Or second option simply encode by yourself using:
echo -n «username@test.com:password» | base64
About your second question:
It is always a good idea to keep those manifests (the whole 07_TektonGCP directory) in a repositoy in order to track changes.
Be aware that you should not push the secrets as is but rather using vault, sealedSecrets etc to store those secrets encrypted the in repository to prevent security leaks.
However, once you have the resources ready you can simply apply them via kubectl apply -f /path/to/your/07_TektonGCP/
I hope this answers your question
Best regards!
Tommy & Christoph