A multi-architecture application is designed to run on multiple architectures, typically arm64 and amd64. The two common ways to build a containerized multi-architecture application is using docker buildx
and docker manifest
.
docker buildx
builds a multi-architecture application at the same time using emulation.docker manifest
joins two different architecture images into a single multi-architecture image.In this learning path you will use the docker manifest
way to build a multi-architecture image.
You can create a Docker repository in the Google Artifact Registry from the Google Cloud UI or with the following command:
gcloud artifacts repositories create quickstart-docker-repo --repository-format=docker \
--location=us-central1 --description="Docker repository" \
--project=<your-project-id>
Replace <your-project-id>
in the command above with your project id in Google Cloud where you want the create the repository.
Configure the cli to authenticate the docker repository in the Artifact Registry:
gcloud auth configure-docker us-central1-docker.pkg.dev
Create the following variables in your GitLab repository by navigating to CI/CD->Variables
. Expand the section and click on Add Variable
.
GCP_PROJECT
- Your Google Cloud project ID that also hosts the Google Artifact registryGKE_ZONE
- Zone for your GKE cluster - e.g. - us-central1-cGKE_CLUSTER
- Name of your GKE clusterIn your GitLab repository, create the following files:
hello.go
- A simple Go
application that prints the architecture of the VM it runs ongo.mod
- Defines the Go
module to use for the applicationDockerfile
- A multi-stage Dockerfile for the application that accepts CPU ARCH as argumentsdeployment.yaml
- The kubernetes deployment file that creates a deployment with multiple replicas of the applicationhello-service.yaml
- Kubernetes yaml file that creates a Service of type LoadBalancer for accessing the applicationCreate hello.go
with the contents below:
package main
import (
"fmt"
"log"
"net/http"
"os"
"runtime"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from image NODE:%s, POD:%s, CPU PLATFORM:%s/%s",
os.Getenv("NODE_NAME"), os.Getenv("POD_NAME"), runtime.GOOS, runtime.GOARCH)
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Create go.mod
with the following content:
module example.com/arm
go 1.21
The Dockerfile
with the following content:
ARG T
#
# Build: 1st stage
#
FROM golang:1.21-alpine AS builder
ARG TARCH
WORKDIR /app
COPY go.mod .
COPY hello.go .
RUN GOARCH=${TARCH} go build -o /hello && \
apk add --update --no-cache file && \
file /hello
#
# Release: 2nd stage
#
FROM ${T}alpine
WORKDIR /
COPY --from=builder /hello /hello
RUN apk add --update --no-cache file
CMD [ "/hello" ]
The deployment.yaml
file references the multi-architecture docker image from the registry. Create the file using the contents below.
apiVersion: apps/v1
kind: Deployment
metadata:
name: multi-arch-deployment
labels:
app: hello
spec:
replicas: 6
selector:
matchLabels:
app: hello
tier: web
template:
metadata:
labels:
app: hello
tier: web
spec:
containers:
- name: hello
image: us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
cpu: 300m
Create a Kubernetes Service of type LoadBalancer
with the following hello-service.yaml
contents:
apiVersion: v1
kind: Service
metadata:
name: hello-service
labels:
app: hello
tier: web
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: hello
tier: web
In this section, the individual Docker images for each architecture - amd64
and arm64
- are built natively on their respective runner. For example, the arm64
build is run on Google Axion runner and vice versa. Each runner is differentiated with tags
in the pipeline. To build a CI/CD pipeline in GitLab, you’ll need to create a .gitlab-ci.yml
file in your repository. Use the following contents to populate the file:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
when: always
- when: never
stages:
- build
- manifest
- deploy
arm64-build:
stage: build
tags:
- arm64
script:
- echo "Building Arm64 Docker image"
- gcloud auth configure-docker us-central1-docker.pkg.dev
- docker build -t us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:arm64 --build-arg TARCH=arm64 --build-arg T=arm64v8/ .
- docker push us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:arm64
amd64-build:
stage: build
tags:
- amd64
script:
- echo "Building x86/amd64 Docker image"
- gcloud auth configure-docker us-central1-docker.pkg.dev
- docker build -t us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:amd64 --build-arg TARCH=amd64 --build-arg T=amd64/ .
- docker push us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:amd64
manifest:
stage: manifest
tags:
- arm64
script:
- echo "Creating single multi-architecture image"
- docker manifest create us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:latest \
--amend us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:arm64 \
--amend us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:amd64
- docker manifest push --purge us-central1-docker.pkg.dev/$GCP_PROJECT/multi-arch-demo/demo-image:latest
deploy:
stage: deploy
tags:
- arm64
script:
- echo "Deploying multi-architecture application to GKE"
- gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GCP_PROJECT
- kubectl apply -f hello-service.yaml
- kubectl apply -f deployment.yaml
This file has three stages
. A stage
is a set of commands that are executed in a sequence to achieve the desired result. In the build
stage of the file there are two jobs that run in parallel. The arm64-build
job gets executed on the Google Axion based C4A runner and the amd64-build
job gets executed on an AMD64 based E2 runner. Both of these jobs push a Docker image to the registry.
In the manifest
stage, using docker manifest
command both of these images are joined to create a single multi-architecture image and pushed to the registry.
In the deploy
stage, this multi-architecture image is deployed on a hybrid GKE cluster - with both arm64
and amd64
nodes.
To execute the pipeline in GitLab, navigate to Build->Pipelines
and click on Run pipeline
. Once the pipeline completes, you should see an arm64, amd64 and a multi-architecture image in Google Artifacts Registry.
Note: If you see the following error after executing the pipeline -
gitlab job error: job failed: prepare environment: exit status 1. check https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading for more information
SSH to the runner VM and execute the following command
sudo vi /home/gitlab-runner/.bash_logout
Comment all the lines in that file, save and exit. Re-run the pipeline again.