Looking for a Cloud Native CI/CD pipelines for your Kubernetes cluster? Check out the tekton-pipelines project from the great folks over at Tekton.

Tekton bridges the ever-decreasing gap between Development and Operations a little bit more with their state of the art pipeline runners and command line interface (which lacks in most modern CI/CD solutions imho, finally start and stop pipeline executions from the CLI)! In this first series of blog posts on tekton (part 1 of 3) we'll setup a Tekton Pipeline to release our public tkn docker image.

Setup

Let's deploy a quick kind cluster using our public kind image, install tekton-pipelines then setup a Pipeline and related tekton resources to build and push our docker images using kaniko after passing our Dockerfile through the Haskell Dockerfile Linter.

Tekton

tekton-pipelines v0.11.0-rc2 is available for download from the projects releases.

KinD

Our kind in dind container mounts the local ${PWD}/tekton directory which should contain the tekton-pipelines release.yaml to the containers /tekton directory. The users docker config (${HOME}/.docker/config.json) also needs to be mounted in the container:

docker run --rm -d \
    --privileged \
    -v ${HOME}/.docker/config.json:/root/.docker/config.json \
    -v ${PWD}/tekton:/tekton \
    --name tekton \
    docker.pkg.github.com/lazybit-ch/kind/kind:v0.7.0

Create the kind cluster: docker exec -it tekton kind create cluster --name tekton then install tekton-pipelines: docker exec -it tekton kubectl apply -f /tekton/release.yaml.

Tip: watch the pods being created in the tekton-pipelines namespace: docker exec -it tekton kubectl get pod -n tekton-pipelines -w.

We need to create an Opaque Secret for the kaniko to access the private registry that we'll be pushing our images to: docker exec -it tekton kubectl -n tekton-pipelines create generic regcred --from-file=/root/.docker/config.json.

Note: if you don't have a docker registry consider running one alongside the kind cluster i.e.: docker exec -it tekton docker run -d -p 5000:5000 --name registry -e REGISTRY_STORAGE_DELETE_ENABLED=true registry:2. In this blog post we'll be pushing images to our official registry.

Tekton Pipelines

tekton-pipelines extends Kubernetes with the Tasks, ClusterTasks, TaskRuns, PipelineResources, Pipelines and PipelineRuns Custom Resources. Breaking down our release pipeline we need to:

  1. checkout the source code
  2. lint the Dockerfile
  3. build the docker image
  4. login to the registry
  5. push the docker image

Task

Tasks define steps to execute and each step of the Task is run in a container. The lint Task uses the git PipelineResource as input to the Task then lints the Dockerfile inside a hadolint/hadolint container (tip: by default objects are created under /workspace and the git repository is cloned to the directory named from the spec.inputs.resources[0].name):

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: hadolint
spec:
  inputs:
    resources:
    - name: source
      type: git
  steps:
  - name: hadolint
    image: hadolint/hadolint
    command: ["/bin/ash"]
    args:
    - -c
    - |
        hadolint /workspace/source/Dockerfile

Tip: splitting the lint and build logic into separate Tasks allows us to share the Tasks in various Pipelines.

The build Task specifies Task scoped parameters with defaults, the Task input (git repository clone) and output (image) resources and the steps to build our image using kaniko:

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: kaniko
  namespace: tekton-pipelines
spec:
  params:
    - name: dockerfile
      type: string
      description: The path to the Dockerfile to build
      default: /workspace/source/Dockerfile
    - name: context
      type: string
      description: The Kaniko build context
      default: /workspace/source
  resources:
    inputs:
      - name: source
        type: git
    outputs:
      - name: image
        type: image
  steps:
    - name: build-and-push
      image: gcr.io/kaniko-project/executor:v0.17.1
      command:
        - /kaniko/executor
      args:
        - --dockerfile=$(params.dockerfile)
        - --destination=$(resources.outputs.image.url)
        - --context=$(params.context)
        - --cache=false
      env:
        - name: DOCKER_CONFIG
          value: /tekton/home/.docker
      volumeMounts:
        - mountPath: /tekton/home/.docker/
          name: regcred
          readOnly: true
  volumes:
    - name: regcred
      secret:
        secretName: regcred

Note: our regcred secret is mounted in the Task spec with the required environment set for kaniko to push the docker image.

TaskRuns

PipelineResources are bound to Tasks during TaskRuns. We can simulate our Pipeline by creating the PipelineResources and TaskRun definitions then applying them to the cluster ordering the execution manually:

apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
  name: tkn-git
  namespace: tekton-pipelines
spec:
  type: git
  params:
    - name: url
      value: https://github.com/lazybit-ch/tkn.git
    - name: revision
      value: master
---
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
  name: hadolint
  namespace: tekton-pipelines
spec:
  taskRef:
    name: hadolint
  resources:
    inputs:
      - name: source
        resourceRef:
          name: tkn-git

After the Dockerfile is linted the PipelineResources and TaskRun for our build can be created then applied to the cluster:

apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
  name: tkn-image
  namespace: tekton-pipelines
spec:
  type: image
  params:
    - name: url
      value: lazybit.ch/tkn:latest
---
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
  name: kaniko
  namespace: tekton-pipelines
spec:
  taskRef:
    name: kaniko
  resources:
    inputs:
      - name: source
        resourceRef:
          name: tkn-git
    outputs:
      - name: image
        resourceRef:
          name: tkn-image

Note: the kaniko TaskRun reuses the tkn-git PipelineResources that were used in the hadolint TaskRun.

Pipelines

Tekton Pipeline resources order our TaskRuns so we don't have to. Our Pipeline definition will ensure that our lint task is executed before executing our build:

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: tkn
spec:
  resources:
    - name: source
      type: git
    - name: image
      type: image
  tasks:
    - name: lint
      taskRef:
        name: hadolint
      resources:
        inputs:
          - name: source
            resource: source
    - name: build
      taskRef:
        name: kaniko
      resources:
        inputs:
          - name: source
            resource: source
        outputs:
          - name: image
            resource: image

PipelineRuns

The Pipelines are triggered by PipelineRuns. Similar to TaskRuns the PipelineRun objects bind Tasks with PipelineResources and executes the Tasks steps in the order specified in the Pipeline:

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: tkn-run
spec:
  pipelineRef:
    name: tkn
  resources:
    - name: source
      resourceRef:
        name: tkn-git
    - name: image
      resourceRef:
        name: tkn-image

Conclusion

This blog post barely scratches the surface of tekton-pipelines but pretty, pretty cool. I like an "all the things in docker" approach to everything and it only makes sense for modern CI/CD (why use Jenkins to build code in docker containers when you could use the docker containers directly). Hard-coded like it is makes it such that the Pipeline will only execute once, we need to delete then create a new PipelineRun to trigger another execution - stay tuned for our future blog post on tekton-triggers to template PipelineRuns with EventListeners!