The Micro in MicroService
MicroServices have been the in thing for a while now. But the question for me is always the size. In my current project, Im doing Operators in Openshift and Kubernetes, and since these in the end are containers and microservices, I want them small.
Im using golang for implementation, and build statically linked executables. So how big is a REST microservice? Are you sitting down? In golang, using a 2 stage docker build, its all of 6.08 Megabytes.
Checkout, winmachineman size. Incredible. This is fully capable of running in OpenShift.
The trick to this is a two-stage build, for this you need the latest docker. In addition you notic the base image is scratch, this is effectively a empty container. We put the golang executable in it. Here's the Dockerfile:
FROM registry.svc.ci.openshift.org/openshift/release:golang-1.10 as builder
RUN go get github.com/glennswest/winmachineman/winmachineman
WORKDIR /go/src/github.com/glennswest/winmachineman/winmachineman
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/winmachineman
FROM scratch
VOLUME /tmp
WORKDIR /root/
COPY --from=builder /go/bin/winmachineman /go/bin/winmachineman
COPY commit.id commit.id
EXPOSE 8080
ENTRYPOINT ["/go/bin/winmachineman"]
The code is pretty simple too:
package main
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"encoding/json"
"fmt"
"log"
)
var router *chi.Mux
func routers() *chi.Mux {
router.Post("/machines", CreateMachine)
router.Delete("/machines/{id}", DeleteMachine)
router.Put("/machines/{id}", UpdateMachine)
router.Get("/machines",AllMachines)
router.Get("/healthz",ReadyCheck)
router.Get("/alivez", AliveCheck)
router.Get("/", HumanUI)
return(router)
}
func init() {
router = chi.NewRouter()
router.Use(middleware.Recoverer)
router.Use(middleware.RequestID)
router.Use(middleware.Logger)
router.Use(middleware.Recoverer)
router.Use(middleware.URLFormat)
}
func ReadyCheck(w http.ResponseWriter, r *http.Request) {
log.Printf("ReadyCheck %s\n", r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "ready"})
}
func AliveCheck(w http.ResponseWriter, r *http.Request) {
log.Printf("ReadyCheck %s\n", r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "alive"})
}
func AllMachines(w http.ResponseWriter, r *http.Request) {
log.Printf("AllMachines %s\n", r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "ok"})
}
func HumanUI(w http.ResponseWriter, r *http.Request) {
log.Printf("HumanUI %s\n", r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "ok"})
}
// Install a New Machine
func CreateMachine(w http.ResponseWriter, r *http.Request) {
log.Printf("CreateMachine: %s\n",r.Body)
respondwithJSON(w, http.StatusCreated, map[string]string{"message": "successfully created"})
}
// UpdateMachine update a specific machine
func UpdateMachine(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
log.Printf("Update Machine: id: %s - %s\n", id, r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "update successfully"})
}
// DeleteMachine - Uninstall a node
func DeleteMachine(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
log.Printf("Uninstall Machine: id:%s %s\n", id, r.Body)
respondwithJSON(w, http.StatusOK, map[string]string{"message": "update successfully"})
}
func main() {
r := routers()
http.ListenAndServe(":8080", r)
}
// respondwithError return error message
func respondWithError(w http.ResponseWriter, code int, msg string) {
respondwithJSON(w, code, map[string]string{"message": msg})
}
// respondwithJSON write json response format
func respondwithJSON(w http.ResponseWriter, code int, payload interface{}) {
response, _ := json.Marshal(payload)
fmt.Println(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}
In order to build it:
dhcp-64-117:winmachineman gwest$ cat build.sh
export GIT_COMMIT=$(git rev-parse --short HEAD)
rm -r -f tmp
mkdir tmp
echo $GIT_COMMIT > commit.id
#eval $(minishift docker-env)
docker build --no-cache -t glennswest/winmachineman:$GIT_COMMIT .
docker tag glennswest/winmachineman:$GIT_COMMIT docker.io/glennswest/winmachineman:$GIT_COMMIT
docker push docker.io/glennswest/winmachineman:$GIT_COMMIT
rm commit.id
dhcp-64-117:winmachineman gwest$
Only special magic is I use the git commit id for version management on the images.
To run it:
dhcp-64-117:winmachineman gwest$ cat run.sh
git pull
export GIT_COMMIT=$(git rev-parse --short HEAD)
echo $GIT_COMMIT
export pname=winmachineman
oc delete dc $pname
oc delete is $pname
oc delete sa $pname
oc delete project $pname
sleep 3
oc new-project $pname
oc import-image $pname --from=docker.io/glennswest/$pname:$GIT_COMMIT --confirm
#oc create sa $pname
#oc adm policy add-cluster-role-to-user cluster-admin system:serviceaccount:$pname:default
#oc policy add-role-to-user admin system:serviceaccount:$pname:default
oc delete istag/$pname:latest
oc new-app glennswest/$pname:$GIT_COMMIT --token=$(oc sa get-token $pname)
export defaultdomain=$(oc describe route docker-registry --namespace=default | grep "Requested Host" | cut -d ":" -f 2 | cut -d "." -f 2-)
oc expose svc/winmachineman --hostname=winmachineman.$defaultdomain
Only magic in the run, is to set the name without the namespace to the running container. This is really cool, and significantly reduces overhead.
The cool news is this trick not only works for golang, but for java and node as well. More to come on hyper-small containers.
Code here: https://github.com/glennswest/winmachineman