DevSecOps and Automation on Power

 View Only

Build multi-arch images on GitHub Actions with Buildx

By Siddhesh Ghadi posted Wed February 08, 2023 02:56 PM

  

If you have ever wondered how to build multi-arch containers to run on ppc64le, x86, ARM , and/or s390x with GitHub Actions, then this article is for you.

GitHub Actions is a continuous integration and continuous development (CI/CD) tool on GitHub that allows users to automate the build, test, and deployment of code. It officially supports building on x86 and ARM CPU architectures via self-hosted runners.

This tutorial is for users who want to build multiple architecture (multi-arch) container images on GitHub Actions. To achieve this, we will cross-compile using Docker Buildx.

Before you begin

Let’s briefly discuss multi-arch images and the Docker Buildx tool.

What is a multiple architecture (multi-arch) image?

Multi-arch container images follow the Build Once, Deploy Anywhere principle. That is, multi-arch images are built to run on multiple CPU platforms without the need to explicitly specify which platform it's running on. The container engine automatically pulls the correct image at runtime based on the host CPU architecture.

How multi-arch images are built

The following links provide more information about multi-arch images.

To build a multi-arch container image, you need access to native hardware. But what if you don’t have access or if adding new hardware to the existing CI setup is too complex? In such cases, you can use container build tools like Buildx, Ko, Buildah, and so on, which emulate the target CPU architecture for building images. There are a few limitations and drawbacks to this approach that are discussed at the end of this tutorial.

What is Buildx?

Buildx is a build plugin from Docker that uses QEMU emulation to cross-build container images. QEMU emulates all instructions of a target CPU instruction set on the host processor where it runs.

NOTE: Buildx is supported in Docker from version 19.03 onwards and requires at least Linux kernel version 4.8 for QEMU emulation to perform cross builds.

Now, let's take a quick look at how cross-compilation works by building an IBM Power (ppc64le) image on an x86 host using Docker Buildx.

Prerequisites

Following are the prerequisites for this tutorial:

  • Access to x86 host with Docker installed to build images (recommended operating systems are Ubuntu, Fedora, CentOS, or Debian).
  • Access to an IBM Power host to test the image and prove that cross-compilation is possible.

Estimated time

This tutorial creates a simple Docker image that might take around 15-20 minutes to perform. However, for more complex projects, the time to create a Docker image may be longer. For example, a less complex project might take 5 mins, a moderately complex may take 15-20 minutes, and a very complex project might take 30 minutes or more.

Cross-compile and build ppc64le image on x86 host using Docker Buildx

Following are the steps to cross-compile and build an IBM Power image on a local x86 host using Docker Buildx. These steps demonstrate how cross-compilation works and are not required to build a multi-arch image using GitHub Actions.

Step 1: Set up QEMU static binaries

The following command sets up the QEMU static binaries. QEMU requires static binaries to emulate the target architectures.

root@x86:~# docker run --rm --privileged tonistiigi/binfmt:latest --install all
installing: arm64 OK
installing: riscv64 OK
installing: mips64le OK
installing: mips64 OK
installing: arm OK
installing: s390x OK
installing: ppc64le OK
{
  "supported": [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
    "linux/ppc64le",
    "linux/s390x",
    "linux/386",
    "linux/mips64le",
    "linux/mips64",
    "linux/arm/v7",
    "linux/arm/v6"
  ],
  "emulators": [
    "qemu-aarch64",
    "qemu-arm",
    "qemu-mips64",
    "qemu-mips64el",
    "qemu-ppc64le",
    "qemu-riscv64",
    "qemu-s390x"
  ]
}

Step 2: Set up Buildx builder

Builder is an isolated environment for building images. It requires a driver which is used as a backend to build images. There are multiple drivers supported such as,  Kubernetes, Docker, remote, and so on. You can find a list of all supported drivers here.

In the following code sample:

  • --driver docker-container is used because it builds multi-arch images.
  • --platform linux/amd64, linux/ppc64le aids Buildx in setting up the builder for building x86 and IBM Power images.
  • --driver-opt "network=host" --buildkitd-flags "--allow-insecure-entitlement network.host --allow-insecure-entitlement security.insecure" are optional flags.

You can find more information on Buildx builder options in the docker buildx_create documentation.

root@x86:~# docker buildx create --name multiarch --platform linux/amd64,linux/ppc64le --driver docker-container --driver-opt "network=host" --buildkitd-flags "--allow-insecure-entitlement network.host --allow-insecure-entitlement security.insecure" --use
multiarch

root@x86:~# docker buildx ls
NAME/NODE    DRIVER/ENDPOINT             STATUS   PLATFORMS
multiarch *  docker-container
  multiarch0 unix:///var/run/docker.sock inactive linux/amd64*, linux/ppc64le*
default      docker
  default    default                     running  linux/amd64, linux/386, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6

Step 3: Cross-compile ppc64le image on x86 host

In the following code, the --platform linux/ppc64le flag builds the image for the ppc64le CPU architecture.

By default, Buildx keeps the new image in an isolated environment, hence you must explicitly specify Buildx to either load the image into the local Docker daemon using –-load or push it into an image registry using –-push (on x86 host). Refer to the Docker Buildx build documentation for more information.

Note: You can run these steps on any operating system. You can also replace Buildx with BuildAH. Refer to Building multi-arch container images with GitHub Actions and Buildah.

root@x86:~# docker buildx build --load --platform linux/ppc64le --tag quay.io/siddhesh_ghadi/multi-arch-gha:latest --builder multiarch -f Dockerfile .
[+] Building 9.4s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                                                                   0.1s
 => => transferring dockerfile: 186B                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                      0.1s
 => => transferring context: 2B                                                                                                        0.0s
 => [internal] load metadata for registry.access.redhat.com/ubi8/ubi-minimal:latest                                                    1.6s
 => [1/2] FROM registry.access.redhat.com/ubi8/ubi-minimal:latest@sha256:6e79406e33049907e875cb65a31ee2f0575f47afa0f06e3a2a9316b01ee3  3.2s
 => => resolve registry.access.redhat.com/ubi8/ubi-minimal:latest@sha256:6e79406e33049907e875cb65a31ee2f0575f47afa0f06e3a2a9316b01ee3  0.0s
 => => sha256:c7a5b2f22050c29571ca75bd0770089e48f198610dc27abd217eb457407937fd 1.74kB / 1.74kB                                         0.3s
 => => sha256:77d8a8d64c347832bb6270961f851fb64072b02218d99c93f6c63b3e100b59c7 41.18MB / 41.18MB                                       2.0s
 => => extracting sha256:77d8a8d64c347832bb6270961f851fb64072b02218d99c93f6c63b3e100b59c7                                              1.1s
 => => extracting sha256:c7a5b2f22050c29571ca75bd0770089e48f198610dc27abd217eb457407937fd                                              0.0s
 => [2/2] RUN echo "Container for $(uname -m) CPU architecture." > /opt/output                                                         1.8s
 => exporting to oci image format                                                                                                      2.7s
 => => exporting layers                                                                                                                0.1s
 => => exporting manifest sha256:b7f4563f269a62d3a1c0d8c2bb44c4dfa943680ea02a1d3b0dc026880d86a456                                      0.0s
 => => exporting config sha256:49ab08f4c0263b34b2ce4042c59280cbeb4386dfa08ff7fa5637c025c6f4d1da                                        0.0s
 => => sending tarball                                                                                                                 2.6s
 => importing to docker

Step 4: Verify built image

As shown in the following command, Docker Buildx uses QEMU to emulate the ppc64le CPU instructions on an x86 host virtual machine (VM) to create an IBM Power container image.

root@x86:~# docker images quay.io/siddhesh_ghadi/multi-arch-gha:latest
REPOSITORY                              TAG       IMAGE ID       CREATED         SIZE
quay.io/siddhesh_ghadi/multi-arch-gha   latest    49ab08f4c026   8 minutes ago   119MB

root@x86:~# docker inspect quay.io/siddhesh_ghadi/multi-arch-gha:latest |grep Arch
        "Architecture": "ppc64le",

Build multi-arch image with GitHub Actions

In the first section, you cross-compiled and built an IBM Power image on a local x86 host using Docker Buildx. In this section, you will perform similar steps with the help of GitHub Actions workflow.

You can use a simple Dockerfile to demonstrate multi-arch image build for x86 and IBM Power CPU architecture on GitHub Actions. Before you set up a GitHub Actions workflow for multi-arch image, ensure that the Dockerfile builds without errors on the target platforms, which in this example, are x86 and IBM Power. Build an image on native hardware or perform a cross-build locally as shown in previous steps.

NOTE: To create a multi-arch image, you must ensure that the base image (check in the container registry where the image is hosted) used is available for your target architectures and scripts used to build images or artifacts should compile and install the software corresponding to the target platform in the container.

Step 1: Set up container registry secrets

To add the repository secrets, go to the GitHub repository and navigate to Settings > Secrets > Actions > New repository secret to add repository secrets.

Set up registry secrets

Note: REGISTRY_USERNAME and REGISTRY_PASSWORD secrets contain credentials for pushing container image from the GitHub Actions workflow to your container registry (quay.io).

Step 2: Configure GitHub Actions workflow

The GitHub Actions workflow uses official Docker Actions available in the GitHub Marketplace, to facilitate the process. To configure the GitHub Actions workflow, add the workflow YAML file (build_image.yaml) to the .github/workflows directory in your GitHub repository.

Configure GitHub Actions workflow

Note: If you are new to GitHub Actions, go through the official documentation to understand the basic workflow structure.

The following code snippet indicates steps to configure the GitHub Actions workflow:

  • docker/setup-qemu-action and docker/setup-buildx-action sets up QEMU and Docker Buildx to cross compile the image for IBM Power.
  • docker/login-action uses the secrets configured in  Step 1: Set up container registry secrets to authenticate with the container registry.
  • docker/build-push-action builds the image and provides the platforms option to specify the target architecture. It provides several options to customize the docker builds. The advantage of using docker/build-push-action over the Docker Buildx commands is that it automatically handles architecture tagging, pushing, and creating the manifest file for multi-arch images.
name: Build Multi-arch Image

on: [push, workflow_dispatch]

jobs:
  build:
    name: Build image
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to container registry
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_TOKEN }}
          registry: quay.io

      - name: Build and Push Image
        uses: docker/build-push-action@v3
        with:
          tags: quay.io/siddhesh_ghadi/multi-arch-gha:latest
          platforms: linux/amd64,linux/ppc64le
          push: true

If you do not want to use docker/build-push-action, you can also build the image using the following configuration:

- name: Build & Push Image
        run: |
          docker buildx build --push --platform linux/amd64,linux/ppc64le --tag quay.io/siddhesh_ghadi/multi-arch-gha:latest .
​

Step 3: Run GitHub actions workflow

The following steps explain how to run a GitHub Actions workflow:

  1. The GitHub Actions workflow is configured to trigger a run on each push commit in repository.
    Run GitHub Actions workflow
  2. Click Build image to view the logs.
    Run GitHub Actions workflow - View Logs
  3. Upon completion, the workflow pushes the multi-arch container image to the quay.io image registry.


Note: You can push the image to any container registry, for example, gcr.io, DockerHub, etc.

Run GitHub Actions workflow

Step 4: Verify multi-arch container image

The following steps confirm that the container image that is compatible with x86 is also compatible with IBM Power.

  1. The following command verifies the compatibility of the container image on the x86 architecture.
    root@x86:~# docker pull quay.io/siddhesh_ghadi/multi-arch-gha:latest
    latest: Pulling from siddhesh_ghadi/multi-arch-gha
    a96e4e55e78a: Pull complete
    67d8ef478732: Pull complete
    048ed16a298a: Pull complete
    Digest: sha256:8a2ac0d6ec4edd07ca080bf5fcce6f526f58ba3ba4774c684ef8db988764cb93
    Status: Downloaded newer image for quay.io/siddhesh_ghadi/multi-arch-gha:latest
    quay.io/siddhesh_ghadi/multi-arch-gha:latest
    
    root@x86:~# docker inspect quay.io/siddhesh_ghadi/multi-arch-gha:latest|grep Arch
            "Architecture": "amd64",
    
    root@x86:~# docker run --rm quay.io/siddhesh_ghadi/multi-arch-gha:latest
    Container for x86_64 CPU architecture.
    ​
  2. The following command verifies that the container image is compatible on the IBM Power architecture, hence establishing multi-arch support with GitHub Actions.
    [root@ppc64le ~]# docker pull quay.io/siddhesh_ghadi/multi-arch-gha:latest
    latest: Pulling from siddhesh_ghadi/multi-arch-gha
    77d8a8d64c34: Pull complete
    c7a5b2f22050: Pull complete
    5a8efc2bee46: Pull complete
    Digest: sha256:8a2ac0d6ec4edd07ca080bf5fcce6f526f58ba3ba4774c684ef8db988764cb93
    Status: Downloaded newer image for quay.io/siddhesh_ghadi/multi-arch-gha:latest
    quay.io/siddhesh_ghadi/multi-arch-gha:latest
    
    [root@ppc64le ~]# docker inspect quay.io/siddhesh_ghadi/multi-arch-gha:latest|grep Arch
            "Architecture": "ppc64le",
    
    [root@ppc64le ~]# docker run --rm quay.io/siddhesh_ghadi/multi-arch-gha:latest
    Container for ppc64le CPU architecture.
    ​

Drawbacks of cross-builds

Although, cross-compilation is generally a useful strategy, it has the following drawbacks:

  • Builds using the emulator are typically slower than building natively, mainly because the emulator needs to convert all instructions to the target CPU architecture.
  • A build might sometimes fail due to incorrect emulation of some low-level code. In such cases moving to native hardware is the best option. For example, native assembly code that may depend intrinsically on page size (4k/x86-64, 64k/ppc64le).
  • Cross-compiled artifacts are often poorly optimized because the emulator is executing code for one architecture on a different CPU and thus loses some of the hardware acceleration benefits.

Summary

To summarize:

  • In the first part you cross-compiled and built an IBM Power image on a local x86 host using Docker Buildx (locally).
  • In the second part, you performed similar steps with the help of GitHub Actions workflow establishing multi-arch support in a CI/CD environment.
  • Later, you tested the container on a native machine to ensure that cross-compilation worked successfully.
  • And finally, we listed a few cross-compilation drawbacks.

If you want to include testing on a native machine, you can set up a GitHub Actions self hosted runner and include testing of your container.

Give it a try and let us know how it went in the comments section below. If you need access to a Power VM, take a look at the resources listed in this blog for several options, many of them are free.


#Featured-area-1
#Featured-area-1-home

Permalink