Build Cross-Platform Images with Buildah and Merge-Image

Feature Overview

This guide shows how to build per-architecture images with the buildah Task, then merge them into a single multi-architecture image with the merge-image Task.

Typical flow:

  1. Build linux/amd64 image tag (for example :v1.0.0-amd64)
  2. Build linux/arm64 image tag (for example :v1.0.0-arm64)
  3. Merge source tags into one or more target tags (for example :v1.0.0, :latest)

Use Cases

  • Build and publish multi-architecture images from one code repository.
  • Keep architecture-specific build outputs while exposing one unified release tag.
  • Re-tag and publish multiple target tags in one merge step.

Prerequisites

  • Tekton Pipelines is installed.
  • git-clone, buildah (0.9), and merge-image (0.1) Tasks are installed.
  • You can push images to your registry.
  • Registry credentials are prepared as a Kubernetes Secret.
  • If you use native node-based builds (as shown in this guide), your cluster must have both amd64 and arm64 nodes available.
  • Optional local verification tools: crane, jq.

Step 1: Prepare Registry Credentials

Create a generic secret with config.json:

apiVersion: v1
kind: Secret
metadata:
  name: registry-config
data:
  config.json: <base64-encoded-registry-config-json>

Step 2: Create Pipeline and Run PipelineRun

Update the following values before use:

  • Git repository URL and revision
  • Architecture-specific source image tags and final target image tag
  • Crane runtime image, if you need to replace the default
  • Workspace and secret names

Example Pipeline:

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: e2e-multiarch-build
spec:
  params:
    - name: git-url
      type: string
      description: Git repository URL
    - name: git-revision
      type: string
      default: main
    - name: image-amd64
      type: string
      description: amd64 image reference
    - name: image-arm64
      type: string
      description: arm64 image reference
    - name: target-image
      type: string
      description: Final multi-architecture image reference
    - name: tls-verify
      type: string
      default: "false"
  workspaces:
    - name: shared-workspace
    - name: git-credentials
    - name: registry-credentials
  tasks:
    - name: git-clone
      params:
        - name: url
          value: $(params.git-url)
        - name: revision
          value: $(params.git-revision)
      taskRef:
        resolver: hub
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: git-clone
          - name: version
            value: "0.9"
      workspaces:
        - name: output
          workspace: shared-workspace
        - name: basic-auth
          workspace: git-credentials

    - name: buildah-amd64
      runAfter:
        - git-clone
      taskRef:
        resolver: hub
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: buildah
          - name: version
            value: "0.9"
      params:
        - name: IMAGES
          value:
            - $(params.image-amd64)
        - name: DOCKERFILE
          value: ./Dockerfile
        - name: TLS_VERIFY
          value: $(params.tls-verify)
        - name: FORMAT
          value: docker
      workspaces:
        - name: source
          workspace: shared-workspace
        - name: registryconfig
          workspace: registry-credentials

    - name: buildah-arm64
      runAfter:
        - git-clone
      taskRef:
        resolver: hub
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: buildah
          - name: version
            value: "0.9"
      params:
        - name: IMAGES
          value:
            - $(params.image-arm64)
        - name: DOCKERFILE
          value: ./Dockerfile
        - name: TLS_VERIFY
          value: $(params.tls-verify)
        - name: FORMAT
          value: docker
      workspaces:
        - name: source
          workspace: shared-workspace
        - name: registryconfig
          workspace: registry-credentials

    - name: merge-image
      runAfter:
        - buildah-amd64
        - buildah-arm64
      taskRef:
        resolver: hub
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: merge-image
          - name: version
            value: "0.1"
      params:
        - name: craneImage
          value: registry.alauda.cn:60070/devops/tektoncd/hub/crane:latest
        - name: sourceImages
          value:
            - $(params.image-amd64)
            - $(params.image-arm64)
        - name: targetImages
          value:
            - $(params.target-image)
        - name: tlsVerify
          value: $(params.tls-verify)
      workspaces:
        - name: registry-config
          workspace: registry-credentials

Example PipelineRun:

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: e2e-multiarch-build-run
spec:
  pipelineRef:
    name: e2e-multiarch-build
  params:
    - name: git-url
      value: https://github.com/your-org/your-app
    - name: git-revision
      value: main
    - name: image-amd64
      value: registry.example.com/team/app:v1.0.0-amd64
    - name: image-arm64
      value: registry.example.com/team/app:v1.0.0-arm64
    - name: target-image
      value: registry.example.com/team/app:v1.0.0
    - name: tls-verify
      value: "false"
  # Node placement is configured on the run resource.
  taskRunSpecs:
    - pipelineTaskName: buildah-amd64
      podTemplate:
        # Force this task pod onto amd64 nodes.
        nodeSelector:
          kubernetes.io/arch: amd64
    - pipelineTaskName: buildah-arm64
      podTemplate:
        # Force this task pod onto arm64 nodes.
        nodeSelector:
          kubernetes.io/arch: arm64
  workspaces:
    - name: shared-workspace
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
    - name: git-credentials
      secret:
        secretName: git-credentials
    - name: registry-credentials
      secret:
        # Must match the Secret created in Step 1.
        secretName: registry-config

Key points in this example:

  • The example uses hub resolver references for git-clone, buildah, and merge-image, so the corresponding catalog Tasks must be resolvable in your cluster.
  • image-amd64 and image-arm64 are explicit Pipeline parameters. This keeps the merge input unambiguous and matches how merge-image consumes sourceImages.
  • taskRunSpecs + podTemplate.nodeSelector are configured in the PipelineRun and are critical when your cluster has mixed CPU architectures. They make sure each build task runs on the matching node architecture.
  • By default, Tekton uses coschedule: workspaces, which creates an Affinity Assistant and tries to place TaskRuns sharing the same PVC-backed workspace on the same node. If you pin buildah-amd64 and buildah-arm64 to different architectures, this default behavior conflicts with the per-task nodeSelector. In that case, set spec.pipeline.coschedule: disabled in your TektonConfig so Tekton stops forcing shared-workspace TaskRuns onto one node. The operator will propagate that setting to the feature-flags ConfigMap automatically.
  • Disabling coschedule is necessary for cross-architecture scheduling, but it is not sufficient on its own. If the shared workspace is backed by a ReadWriteOnce PVC, Kubernetes still cannot mount that volume read-write on two different nodes at the same time. To run the amd64 and arm64 builds concurrently on different nodes, use a workspace backend that supports multi-node sharing such as ReadWriteMany.
  • sourceImages should point to the architecture-specific tags, while targetImages should be the final multi-arch tag or tags you want to publish.
  • registry-credentials workspace is reused by both buildah and merge-image; the workspace name mapping (registryconfig vs registry-config) is task-interface specific and must match each Task definition.

Step 3: Verify Merged Image Platforms

After PipelineRun succeeds, you can verify the merged image platforms with crane, podman, or skopeo:

Using crane:

crane manifest registry.example.com/team/app:v1.0.0 \
  | jq -r '.manifests[].platform | "\(.os)/\(.architecture)"'

Using podman:

podman manifest inspect docker://registry.example.com/team/app:v1.0.0 \
  | jq -r '.manifests[].platform | "\(.os)/\(.architecture)"'

Using skopeo:

skopeo inspect --raw docker://registry.example.com/team/app:v1.0.0 \
  | jq -r '.manifests[].platform | "\(.os)/\(.architecture)"'

Expected output includes:

  • linux/amd64
  • linux/arm64

Troubleshooting

  • If buildah-amd64 and buildah-arm64 share one PVC-backed workspace and are pinned to different architectures, check the Tekton Pipelines feature flag coschedule. Set spec.pipeline.coschedule: disabled in your TektonConfig when you need those TaskRuns to schedule onto different nodes; the default workspaces mode tries to co-locate them on one node through Affinity Assistant. For the exact setting location and behavior, see Unable to Use Multiple PVC Workspaces in Tekton.
  • Even with coschedule: disabled, a ReadWriteOnce PVC still cannot be mounted read-write from two different nodes simultaneously. For true parallel cross-architecture builds, bind the shared workspace to storage that supports ReadWriteMany.
  • Prefer keeping sourceImages and targetImages in the same registry. Cross-registry input is allowed but merge-image emits warning logs.
  • merge-image allows cross-repository merges. For cross-registry source or target images, it continues with warning logs, so make sure credentials are valid for every registry involved.
  • sourceImages and targetImages must be non-empty. Duplicate source references or duplicate source digests are skipped, and at least one unique source image must remain.
  • For self-signed registries:
    • buildah: mount CA files to sslcertdir
    • merge-image: mount CA files to ca-bundle and optionally set caFileName
    • set TLS_VERIFY / tlsVerify to false only in trusted environments
  • buildah parameters are uppercase (for example IMAGES, CONTAINERFILE), while merge-image uses lower camel case (for example sourceImages, targetImages).