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!