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:
- on the controller
pi@ctrl:~ $ docker swarm init --advertise-addr <ctrl_ip_address>
- on each of the nodes:
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!