Docker Swarm on ClusterHAT

Author: Carmelo C.
Published: Sep 23, 2024 (updated)
docker armv6 armv7 arm64 aarch64 containers

INDEX

Installation of ClusterHAT has been described in a previous post.

The basics: hardware architecture

Controller:

pi@ctrl $ docker info | grep Architecture
 Architecture: armv7l

Pi Zeros:

pi@zero $ docker info | grep Architecture
 Architecture: armv6l

Notice how the Controller is based on ARMv7 while the Zeros are based on ARMv6.

Our app, an HTTP server:

File Dockerfile.armv6:

FROM arm32v6/golang:alpine AS builder
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o goweb.go .

FROM arm32v6/alpine
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/goweb.go ./
CMD ["./goweb.go"]

NOTE: the code above is for a multi-stage build. Read more on the topic on docs.docker.com.

File goweb.go:

package main

import (
	"fmt"
	"net/http"
	"os"
	"runtime"
	"strings"
)

var listenPort = ":8888"

func sayHello(w http.ResponseWriter, r *http.Request) {
	hostname, _ := os.Hostname()
	message := r.URL.Path
	message = strings.TrimPrefix(message, "/")
	message = "This is " + hostname +
                  " running on " + runtime.GOOS +
                  "/" + runtime.GOARCH + " saying: " +
                  message + "\n"
	fmt.Printf("GoWeb: got / request\n")
	w.Write([]byte(message))
}

func main() {
	http.HandleFunc("/", sayHello)
	if err := http.ListenAndServe(listenPort, nil); err != nil {
		panic(err)
	}
}

NOTE: as a quick start, you can lazily clone this repository.

The first step is to build an image on the Controller and run it. During this tutorial I’ll be using ARM32v6 (armv6l) images since the Controller and the Nodes do not share a common hardware architecture. This topic, along with Buildx, are covered in another article.

pi@ctrl $ docker build -t <repo>/goweb:1.0 -f Dockerfile.armv6 .
...
Successfully built 1f4eef919f7b
Successfully tagged <repo>/goweb:1.0

pi@ctrl $ docker image ls
REPOSITORY       TAG      IMAGE ID       CREATED          SIZE
<repo>/goweb     1.0      7f4cb5b0ba93   12 minutes ago   12.3MB <- new image
arm32v6/golang   alpine   4fdc1d7c0d8c   2 weeks ago      240MB  <- builder image

NOTE: a comparison between the size of our image and the arm32v6/golang image shows a dramatic reduction is size. This is the main benefit of multi-stage build.

Let’s run our image:

pi@ctrl $ docker run -d --name goweb -p 8080:8888 <repo>/goweb:1.0
a8c0d0b5c784251f2afb374c3ed156095ed82004f8501bb4887bb5dedecb07e4

… and from any host connected to the same subnet:

user@host $ curl http://<ctrl_ip_address>:8080/Hello!
This is a8c0d0b5c784 running on linux/arm saying: Hello!

Notice how the hostname matches the container ID on the controller:

pi@ctrl $ docker container ls
CONTAINER ID   IMAGE             COMMAND    CREATED         STATUS        PORTS                      NAMES
a8c0d0b5c784   <repo>/goweb:1.0  "./goweb"  2 minutes ago   Up 2 minutes  0.0.0.0:8080->8888/tcp...  goweb

Once we’re satified with our image, we can push it to Docker Hub:

pi@ctrl $ docker login -u <repo>

pi@ctrl $ docker push <repo>/goweb:1.0

NOTE: the step above is quite important because Docker Hub doesn’t currently build images for ARM processors.

BONUS POINT: inspecting the image can show very interesting info, as an example:

pi@ctrl $ docker image inspect --format "{{.Architecture}}{{.Variant}}" <repo>/goweb:1.0
armv6

Docker Swarm, finally!

Docker swarm mode refers to the cluster management and orchestration features embedded in the Docker Engine. The swarm creation part is well documented, nonethelesshere’s a quick refresher:

pi@ctrl:~ $ docker swarm init --advertise-addr <ctrl_ip_address>
pi@zeroX:~ $ docker swarm join --token *** <ctrl_ip_address>:2377

Let’s now focus on running and scaling our application and let’s start by taking a look at our cluster:

pi@ctrl $ docker node ls
ID                  HOSTNAME  STATUS      AVAILABILITY    MANAGER STATUS    ENGINE VERSION
74u4z2qw95w0ctz *   ctrl      Ready       Active          Leader            25.0.1
u0ayftfqi4z728p     zero1     Ready       Active                            25.0.1
go7epn59usoc50f     zero2     Ready       Active                            25.0.1
kz267mzscz0ol8h     zero3     Ready       Active                            25.0.1
rv24m6fuipawylf     zero4     Ready       Active                            25.0.1

Let’s create a new service as follows:

pi@ctrl $ docker service create --name goweb --replicas 5 --publish published=8080,target=8888 <repo>/goweb:1.0
ox51ocpvar7sx1f
overall progress: 5 out of 5 tasks
1/5: running   [==================================================>]
...
5/5: running   [==================================================>]
verify: Service ox51ocpvar7sx1f converged

The service can be displayed and inspected as follows:

pi@ctrl $ docker service ls
ID              NAME     MODE          REPLICAS    IMAGE              PORTS
ox51ocpvar7s    goweb    replicated    5/5         <repo>/goweb:1.0   *:8080->8888/tcp

pi@ctrl $ docker service ps goweb
ID             NAME      IMAGE              NODE    DESIRED STATE   CURRENT STATE               ERROR  PORTS
i5ky88du3kbp   goweb.1   <repo>/goweb:1.0   zero1   Running         Running about a minute ago
...
fmm9uf112y6j   goweb.4   <repo>/goweb:1.0   zero4   Running         Running about a minute ago
w44yl47vtnzi   goweb.5   <repo>/goweb:1.0   ctrl    Running         Running about a minute ago

pi@ctrl $ docker service inspect --format="{{json .Endpoint.Spec.Ports}}" goweb
[{"Protocol":"tcp","TargetPort":8888,"PublishedPort":8080,"PublishMode":"ingress"}]

pi@ctrl $ docker service inspect --format="{{json .Spec.TaskTemplate.ContainerSpec.Image}}" goweb
"<repo>/goweb:1.0@sha256:***"

The test is, once agin, run from a different host. Notice how five replicas are responding in round-robin fashion:

user@host $ curl http://<ctrl_ip_address>:8080/Hello\!
This is 0801fa03baa6 running on linux/arm saying: Hello!

user@host $ curl http://<ctrl_ip_address>:8080/Hello\!
This is d3ffa4006aa8 running on linux/arm saying: Hello!

user@host $ curl http://<ctrl_ip_address>:8080/Hello\!
This is fbd04731918f running on linux/arm saying: Hello!

...

user@host $ curl http://<ctrl_ip_address>:8080/Hello\!
This is 0801fa03baa6 running on linux/arm saying: Hello!

Just like before, the hostnames match the actual containers running on the various nodes.

BONUS POINT: sending a space can be attained by replacing " " with “%20” as such:

user@host $ curl http://<ctrl_ip_address>:8080/Hello%20there\!
This is d3ffa4006aa8 running on linux/arm saying: Hello there!