Build container images (and push them to a registry) in Kubernetes with Tekton

tekton build push Kubernetes

5 min read | by Jordi Prats

We can build containers from within a Kubernetes cluster using Kaniko using Pods or use some framework to streamline the process such as Shipwright that uses tekton pipelines to actually run the process. We can skip Shipwright and create directly tekton pipelines.

Install tekton

First we'll need to install tekton, to do so we just need to apply the following manifests:

kubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml

Create secrets

To be able to work with private repositories and registries we'll have to push som secrets:

Private registry

To configure a private registry we'll need to create a secret with the following type: kubernetes.io/dockerconfigjson. The data it requires is the following:

kubectl create secret docker-registry private-registry \
                        --docker-server=$REGISTRY_SERVER \
                        --docker-username=$REGISTRY_USER \
                        --docker-password=$REGISTRY_PASS \
                        --docker-email=$REGISTRY_EMAIL

Private repository

In order to tell tekton to use a given private ssh key for github.com we'll need to create a secret as follows:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: github-key
  annotations:
    tekton.dev/git-0: github.com
type: kubernetes.io/ssh-auth
data:
  ssh-privatekey: $(base64 -w0 ~/.ssh/id_rsa)
EOF

Tekton ServiceAccount

In order to let tekton have these secrets we are going to create a ServiceAccount referencing the secrets:

cat <<"EOF" | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: pipeline-runner
secrets:
- name: private-registry
- name: github-key
EOF

Import tasks

To actually define what we want to run we can do so using tekton tasks either by defining it from scratch or reusing one from tekton hub. For this demo we are going to reuse publicly available git-clone and the kaniko tasks:

kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.3/git-clone.yaml
kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/kaniko/0.2/kaniko.yaml

We can check it's yaml definition to see what it basically does is to run a container. For example, the kaniko task looks like this:

$ kubectl get task kaniko -o yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: kaniko
(...)
spec:
  description: |-
    This Task builds source into a container image using Google's kaniko tool.
    Kaniko doesn't depend on a Docker daemon and executes each command within a Dockerfile completely in userspace. This enables building container images in environments that can't easily or securely run a Docker daemon, such as a standard Kubernetes cluster.
  params:
  - description: Name (reference) of the image to build.
    name: IMAGE
    type: string
  - default: ./Dockerfile
    description: Path to the Dockerfile to build.
    name: DOCKERFILE
    type: string
  - default: ./
    description: The build context used by Kaniko.
    name: CONTEXT
    type: string
  - default: ""
    name: EXTRA_ARGS
    type: string
  - default: gcr.io/kaniko-project/executor:v1.5.1@sha256:c6166717f7fe0b7da44908c986137ecfeab21f31ec3992f6e128fff8a94be8a5
    description: The image on which builds will run (default is v1.5.1)
    name: BUILDER_IMAGE
    type: string
  results:
  - description: Digest of the image just built.
    name: IMAGE-DIGEST
    type: string
  steps:
  - args:
    - $(params.EXTRA_ARGS)
    - --dockerfile=$(params.DOCKERFILE)
    - --context=$(workspaces.source.path)/$(params.CONTEXT)
    - --destination=$(params.IMAGE)
    - --oci-layout-path=$(workspaces.source.path)/$(params.CONTEXT)/image-digest
    computeResources: {}
    env:
    - name: DOCKER_CONFIG
      value: /tekton/home/.docker
    image: $(params.BUILDER_IMAGE)
    name: build-and-push
    securityContext:
      runAsUser: 0
    workingDir: $(workspaces.source.path)
(...)

Once we have them in the namespace, we'll be able to start creating the pipeline definition:

$ kubectl get tasks
NAME        AGE
git-clone   6s
kaniko      3s

Pipeline definition

The pipeline definition (Pipeline object) will hold that tasks that it is going to run and the parameters it is going to need to configure it:

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: build-and-deploy-pipeline
spec:
  workspaces:
    - name: git-source
      description: The git repo
  params:
    - name: gitUrl
      description: Git repository url
      type: string
    - name: gitRevision
      description: Git revision to check out
      type: string
      default: master
    - name: pathToContext
      description: The path to the build context
      default: src
      type: string
    - name: imageUrl
      description: Image name including repository
      type: string
    - name: imageTag
      description: Image tag
      type: string
      default: latest
  tasks:
    - name: clone-repo
      taskRef:
        name: git-clone
      workspaces:
        - name: output
          workspace: git-source
      params:
        - name: url
          value: "$(params.gitUrl)"
        - name: revision
          value: "$(params.gitRevision)"
        - name: subdirectory
          value: "."
        - name: deleteExisting
          value: "true"
        - name: verbose
          value: "true"
    - name: build-and-push-image
      taskRef:
        name: kaniko
      runAfter:
        - clone-repo
      workspaces:
        - name: source
          workspace: git-source
      params:
        - name: CONTEXT
          value: $(params.pathToContext)
        - name: IMAGE
          value: $(params.imageUrl):$(params.imageTag)

Pipeline execution

To actually run the pipeline, building and pushing the container image to the private registry we'll have to create a PipelineRun object, specifying the ServiceAccount with the appropriate secrets:

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: build-and-deploy-execution
spec:
  pipelineRef:
    name: build-and-deploy-pipeline
  params:
    - name: gitUrl
      value: git@github.com:pet2cattle/demo-app.git
    - name: gitRevision
      value: HEAD
    - name: imageUrl
      value: repo.pet2cattle.com/demo-app
    - name: imageTag
      value: latest
    - name: pathToContext
      value: '.'
  serviceAccountName: pipeline-runner
  workspaces:
    - name: git-source
      volumeClaimTemplate:
        spec:
          accessModes: 
          - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi

As soon as this object gets picked up, it will create a volumeClaim to hold the temporal data it uses and start running the Pods with each of the tasks that we have configured with the Pipeline template. We can check it's status using kubectl get pipelinerun:

$ kubectl get pipelinerun
NAME                         SUCCEEDED   REASON               STARTTIME   COMPLETIONTIME
build-and-deploy-execution   Unknown     Running              10s
$ kubectl get pipelinerun
NAME                         SUCCEEDED   REASON               STARTTIME   COMPLETIONTIME
build-and-deploy-execution   True        Succeeded            48m         44m

Posted on 04/01/2023