Multiarch build en k3s con tekton

Mi hijo me pidió que le arme un blog, como me gusta complicar las cosas, esto me llevó a revivir una vieja raspberry que tenía problemas con el voltaje y por lo tanto no estaba usando, y aprovechando unos días de vacaciones, me puse a trabajar (era el cable el problema, y no la placa como alguna vez pensé).

Durante muchos años tuve este blog en linea corriendo en su propio hardware, en ese momento me parecía buena idea mantenerme con la arquitectura ARM v7 (32 bits) ya que era lo mas popular por fuera del mundo intel/amd, pero siendo que estamos en 2023, cobra sentido montar algo utilizando una arquitectura acorde a los tiempos que vivimos, y decidí armar una imagen ARM v8 (64 bits).

Hace varios años había armado con un gran amigo a Ñamandú, una solución que por lo divertido que estuvimos los últimos tres años no hemos vuelto a tocar, al medio era obvio que alguna solución iba a aparecer y que el costo de actualizar lo que habíamos hecho iba a ser superior a adaptar algo ya existente, es por eso que me puse a ver tekton y a retomar las pruebas de multiarch building en el universo de buildah.

Escapa al objetivo de este texto cubrir los fundamentos de tekton, que se pueden encontrar en la documentación oficial, me voy a enfocar en dejarme documentado para mi yo del futuro, las modificaciones que tuve que realizar sobre la task de buildah oficial

Lo primero fue entender cuáles eran los comandos necesarios para hacer un build con buildah, generando un manifiesto mutliarquitectura, subiéndolo luego junto con las imágenes resultantes a un registro.

Modificaciones en la task

Dentro del script de la task, lo primero que hice fue agregar una configuración en /etc/containers/registries.conf que permitiese buscar imágenes en el registry de docker (docker.io), de esta forma si queremos buildear imágenes con referencias del tipo FROM alpine:latest no tendremos que modificarlas a algo como FROM docker.io/alpine:latest, como la idea es buildear imágenes de terceros, esto nos puede ser de mucha utilidad.

echo 'unqualified-search-registries = ["docker.io"]' >> /etc/containers/registries.conf

Luego, le agrego al script la creación de un manifest, utilizando buildah manifest create multiarchmanifest (el nombre del manifest no me importa, ya que lo uso temporalmente durante el tiempo de vida de la ejecución del pipeline).

Posteriormente, modifico la linea de buildah bud para incluir el uso del manifest creado:

buildah ${CERT_DIR_FLAG} --storage-driver=$(params.STORAGE_DRIVER) bud \
      $(params.BUILD_EXTRA_ARGS) --format=$(params.FORMAT) --manifest multiarchmanifest \
        --tls-verify=$(params.TLSVERIFY) --no-cache \
      -f $(params.DOCKERFILE) -t $(params.IMAGE):$(params.TAG) $(params.CONTEXT)

Luego reemplazo la linea de push (en el archivo final dejo los comments para que se entienda dónde fue el reemplazo), por un buildah manifest push

buildah ${CERT_DIR_FLAG} --storage-driver=$(params.STORAGE_DRIVER) manifest push \
               $(params.PUSH_EXTRA_ARGS) --tls-verify=$(params.TLSVERIFY) \
               --digestfile /tmp/image-digest --rm multiarchmanifest \
               --format=$(params.FORMAT) docker://$(params.IMAGE):$(params.TAG) 

También agregué un parámetro TAG a la tarea, de forma tal de que pueda luego definir en el pipelinerun el tag multiarch que quiero construir.

El archivo resultante queda entonces definido como:

---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: buildah
  labels:
    app.kubernetes.io/version: "0.5"
  annotations:
    tekton.dev/categories: Image Build
    tekton.dev/pipelines.minVersion: "0.17.0"
    tekton.dev/tags: image-build
    tekton.dev/platforms: "linux/amd64,linux/s390x,linux/ppc64le,linux/arm64"
spec:
  description: >-
    Buildah task builds source into a container image and
    then pushes it to a container registry.

    Buildah Task builds source into a container image using Project Atomic's
    Buildah build tool.It uses Buildah's support for building from Dockerfiles,
    using its buildah bud command.This command executes the directives in the
    Dockerfile to assemble a container image, then pushes that image to a
    container registry.

  params:
  - name: IMAGE
    description: Reference of the image buildah will produce.
  - name: BUILDER_IMAGE
    description: The location of the buildah builder image.
    default: quay.io/buildah/stable:v1.30.0
  - name: STORAGE_DRIVER
    description: Set buildah storage driver
    default: overlay
  - name: DOCKERFILE
    description: Path to the Dockerfile to build.
    default: ./Dockerfile
  - name: CONTEXT
    description: Path to the directory to use as context.
    default: .
  - name: TLSVERIFY
    description: Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry)
    default: "true"
  - name: FORMAT
    description: The format of the built container, oci or docker
    default: "oci"
  - name: BUILD_EXTRA_ARGS
    description: Extra parameters passed for the build command when building images.
    default: ""
  - name: PUSH_EXTRA_ARGS
    description: Extra parameters passed for the push command when pushing images.
    type: string
    default: ""
  - name: SKIP_PUSH
    description: Skip pushing the built image
    default: "false"
  - name: TAG
    description: Image tag 
    default: "latest"
  workspaces:
  - name: source
  - name: sslcertdir
    optional: true
  - name: dockerconfig
    description: >-
      An optional workspace that allows providing a .docker/config.json file
      for Buildah to access the container registry.
      The file should be placed at the root of the Workspace with name config.json.
    optional: true
  results:
  - name: IMAGE_DIGEST
    description: Digest of the image just built.
  - name: IMAGE_URL
    description: Image repository where the built image would be pushed to
  steps:
  - name: build
    image: $(params.BUILDER_IMAGE)
    workingDir: $(workspaces.source.path)
    script: |
      # add some extra config to search on docker.io and don't broke already well knowed images
      echo 'unqualified-search-registries = ["docker.io"]' >> /etc/containers/registries.conf

      [[ "$(workspaces.sslcertdir.bound)" == "true" ]] && CERT_DIR_FLAG="--cert-dir $(workspaces.sslcertdir.path)"
      [[ "$(workspaces.dockerconfig.bound)" == "true" ]] && export DOCKER_CONFIG="$(workspaces.dockerconfig.path)"
      buildah manifest create multiarchmanifest 
      buildah ${CERT_DIR_FLAG} --storage-driver=$(params.STORAGE_DRIVER) bud \
      $(params.BUILD_EXTRA_ARGS) --format=$(params.FORMAT) --manifest multiarchmanifest \
        --tls-verify=$(params.TLSVERIFY) --no-cache \
      -f $(params.DOCKERFILE) -t $(params.IMAGE):$(params.TAG) $(params.CONTEXT)
      [[ "$(params.SKIP_PUSH)" == "true" ]] && echo "Push skipped" && exit 0
      #      buildah ${CERT_DIR_FLAG} --storage-driver=$(params.STORAGE_DRIVER) push \
      #        $(params.PUSH_EXTRA_ARGS) --tls-verify=$(params.TLSVERIFY) \
      #        --digestfile /tmp/image-digest $(params.IMAGE):$(params.IMAGE) \
      #        docker://$(params.IMAGE):$(params.IMAGE) 
      buildah ${CERT_DIR_FLAG} --storage-driver=$(params.STORAGE_DRIVER) manifest push \
               $(params.PUSH_EXTRA_ARGS) --tls-verify=$(params.TLSVERIFY) \
               --digestfile /tmp/image-digest --rm multiarchmanifest \
               --format=$(params.FORMAT) docker://$(params.IMAGE):$(params.TAG) 
 
      cat /tmp/image-digest | tee $(results.IMAGE_DIGEST.path)
      echo -n "$(params.IMAGE)" | tee $(results.IMAGE_URL.path)
    volumeMounts:
    - name: varlibcontainers
      mountPath: /var/lib/containers
    securityContext:
      privileged: true
  volumes:
  - name: varlibcontainers
    emptyDir: {}

Creando el pipeline

Vamos ahora a definir un archivo para alimentar tekton, para que este cree un pipelinerun. En el mismo llamamos a la tarea oficial de git-clone, con parámetros especifiando el repositorio de código que queremos construir, el nombre de la imagen (incluyendo el registry), y algunos parámetros adicionales para el build, y otros para el push.

Para el build vamos a agregarle --jobs 3 --platform linux/arm,linux/arm64,linux/amd64, esto define las plataformas para las que vamos a construir la imagen, y adicionalmente le paso la cantidad de jobs en paralelo que quiero que ejecute (3, por las 3 plataformas).

Para el caso del push, el parámetro --all permite que al hacer push del manifest, también se suban las imágenes contenidas en el índice de dicho manifesto.

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  generateName: buildah-writefreely-pipeline-run-
spec:
  pipelineSpec:
    workspaces:
      - name: shared-workspace
      - name: sslcertdir
        optional: true
      - name: dockerconfig-ws
        optional: true
    tasks:
      - name: fetch-repository
        taskRef:
          name: git-clone
        workspaces:
          - name: output
            workspace: shared-workspace
        params:
          - name: url
            value: https://github.com/writefreely/writefreely.git 
          - name: subdirectory
            value: ""
          - name: deleteExisting
            value: "true"
      - name: buildah
        taskRef:
          name: buildah
        runAfter:
        - fetch-repository
        workspaces:
        - name: source
          workspace: shared-workspace
        - name: dockerconfig
          workspace: dockerconfig-ws
        params:
        - name: IMAGE
          value: docker.io/jinetessl/writefreely 
        - name: BUILD_EXTRA_ARGS
          value: --jobs 3 --platform linux/arm,linux/arm64,linux/amd64
        - name: PUSH_EXTRA_ARGS
          value: --all
        - name: SKIP_PUSH
          value: false 
        - name: FORMAT
          value: docker
  workspaces:
    - name: shared-workspace
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1000Mi
    - name: dockerconfig-ws
      secret:
        secretName: dockerconfig-secret

Adicionalmente armé un secreto con las credenciales de auth para poder subir al registro de docker las imágenes resultantes:

apiVersion: v1
kind: Secret
metadata:
  name: dockerconfig-secret
stringData:
  config.json: |
    {
      "auths": {
        "docker.io": {
          "auth": "XXXXXXXXXXXXXXX=="
        }
      }
    }

Luego, tras realizar un kubectl create -f con el archivo del pipeline run, tras unos minutos tengo una imagen multiarchitectura subida en el repositorio de imágenes, un amor!