Go, Docker, Google Cloud: A Microservice HOWTO

Saying that software engineers today enjoy MicroServices (MS) ‘a little bit’ is like saying people enjoy chocolate covered bacon ‘a little bit’. MicroServices does for internet systems what function calls do for assembly: abstraction without losing functionality.

A confluence of fortunate technologies arrived in the last few years that can make MS possible or at least much more easier. Docker is the biggest one. The others can start holy wars so I’ll just remain neutral. I write what I know, OK?

We’re going to use a simple MS example that generates ascii art from an uploaded image using Docker, Go, and Google Cloud Platform.

Go fork the (https://github.com/rockviper/img2asciiart), and let’s get started! :D

Go is made for MicroServices

Writing C on UNIX is what God intended but Go on a Docker image is both fun and minimally unorthodox so you only need to pay a minimal penance. Let’s go for it!

Go was written by Google for a lot of the kind of problems a place like Google would face: lots of scalable software that is easy to build and deploy. Building Go programs is waaay easier than it was with C/C++. No complex linking. And co-routines are a dream.

Here is a very basic, simplistic microservice. Serve up a path and output a result while listening on port 8080.

package main

import (
"fmt"
"net/http"
"image"
_ "image/jpeg"
_ "image/gif"
"image/color"
"github.com/nfnt/resize"
"bytes"
"reflect"
"log"
"strconv"
)


func handler(w http.ResponseWriter, r *http.Request) {

if r.Method == "POST" {
dimension, err := strconv.Atoi(r.URL.Path[1:])
if err != nil {
dimension = 80
}
if (dimension > 256) || (dimension < 0) {
dimension = 256
}
file, _, err := r.FormFile("uploadfile")
if err != nil {
fmt.Println(err)
return
}

defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
log.Print(err)
return
}
fmt.Fprintf(w, "%s",
Convert2Ascii(ScaleImage(img, dimension)))
}
}

// https://github.com/stdupp/goasciiart
var ASCIISTR = "MND8OZ$7I?+=~:,.."

// https://github.com/stdupp/goasciiart
func ScaleImage(img image.Image, w int) (image.Image, int, int) {
sz := img.Bounds()
h := (sz.Max.Y * w * 10) / (sz.Max.X * 16)
img = resize.Resize(uint(w), uint(h), img, resize.Lanczos3)
return img, w, h
}

// https://github.com/stdupp/goasciiart
func Convert2Ascii(img image.Image, w, h int) []byte {
table := []byte(ASCIISTR)
buf := new(bytes.Buffer)

for i := 0; i < h; i++ {
for j := 0; j < w; j++ {
g := color.GrayModel.Convert(img.At(j, i))
y := reflect.ValueOf(g).FieldByName("Y").Uint()
pos := int(y * 16 / 255)
_ = buf.WriteByte(table[pos])
}
_ = buf.WriteByte('\n')
}
return buf.Bytes()
}

func main() {
http.HandleFunc("/v1", handler)
http.ListenAndServe(":8080", nil)
}

Building the Docker Image

You should note the github repository is arranged like this.

.
├── Dockerfile
├── README.md
└── image2asciiart.go

Building the docker image is as simple as this

docker build -t us.gcr.io/goasciiart-171107/my-golang-app .

Typically a directory structure for a docker build would have a Dockerfile in it’s own directory. This is by design

The <src> path must be inside the context of the build; you cannot ADD ../something /something, because the first step of a docker build is to send the context directory (and subdirectories) to the docker daemon.

You can use the -f option to point to another directory.

.
├── README.md
├── build
│ ├── Dockerfile
│ ├── asdf.exe
│ └── data/
├── node.py
└── tests

You can run the docker image locally:

docker run -it -p 8080:8080 --rm --name my-running-app us.gcr.io/goasciiart-171107/my-golang-app

And test it out, issue a curl command like so:

$ curl -F "uploadfile=@wilder.jpg" localhost:8080/
I=77$I$7$Z$$ZZZI:.:=77$OZ8DNN8O88
$+7$$777$Z$$OOZ$?,,+7$ODNM88DOO88
OZOOOOZZ$ZOO8OZZ$$$7$O8D888D8OON8
OZZZ$$77$$$I???+++++??II77$$$$OND
++++++=IODNI+?+??IIIII77$$$ZZZO8D
I???????ZOOIII????III777777$$$Z$$
?++?+?++I8D8$?+=IOZ????III777$$$Z
IIIIIIII7OMMN$IZDNZI77IIII7$7$$77
II77III7I$$$7I$ZZOO7Z$?I7I7??I777
77I$I77777777$$ZOOZ77777$$77$ZZ$7
7$77777777777I777$7777777I777$$$$
$$77$77777777$$7777$77$777777$$$Z
77777$Z$$$$$$$777$$7$$7777$ZZ$ZZZ
$

Note that the -F option is used to submit a form object with a parameter called uploadfile.

Google Compute Cloud

AWS feels like a Chinese place that also sells friend chicken and pizza. The menu is all over the place; it was fun at first but half a score years later, it’s overwhelming. AWS is the place that you recommend to everyone else except your self. AWS: a platform that both is and isn’t technically obtuse until observed. I look at the Google Compute Cloud environment and…and I think the future is going to be OK.

gcloud Command Line Tool

First thing’s first: install the gcloud utility. gcloud makes it easier to interface with GCP compared to the rainbow of other cli tools that we’ve come to be used to.

GCP works in ‘projects’. This keeps resources split. If you upload a docker image to the wrong project, the other projects won’t necessarily see it.

Create a project

Eventually you’ll get a project built.

Take note of this number

Do you see the highlighted img2ascii-171905 code? Make note of it.

There are a few more moving parts we’ll need.

Docker Compute Registry

Remember back when we built the docker image? We need to tag it in a particular way. GCP wants it that way.

docker build -t us.gcr.io/img2ascii-171905/my-golang-app .

We need to now push the docker image to the Google cloud

gcloud docker — push us.gcr.io/goasciiart-171107/my-golang-app

Kubernetes

Kubernetes is the main way to get clusters of Docker images running in the cloud, either Google’s cloud or not. Google’s offering is called Google Container Engine.

Google talk for Kubernetes

Creating a Container Engine Cluster

Creating a cluster is super easy with gcloud.

gcloud config set compute/zone us-central1-b

Creating a default-configured container cluster will result in 3 new instances of VMs running. These will cost you money.

gcloud container clusters create img2ascii

gcloud auth application-default login

kubectl run img2ascii-node --image=gcr.io/google-samples/node-hello:1.0 --port=8080

Be forewarned that the LoadBalancer can actually cost you extra money

kubectl expose deployment img2ascii-node --type="LoadBalancer"

kubectl get service img2ascii-node

You should see an external IP listed under EXTERNAL-IP after a few seconds.

Extras

Q: My API doesn’t get exposed to the external environment.

A: If you try to re-run the curl command and get no output or a hung response, what has probably happened is you told kubernetes to run an image it can’t find.

Run glcoud projects list to see if you have other projects created and then use gcloud config set project img2ascii-171905to set the project you want as the default. You should be able to re-run the kubectl run command.

Q: How Can I see the Kubernetes console?

A: Run the following

gcloud containers get-credentials img2ascii

kubectl proxy

You should be able to browse over to localhost:8001/ui and see the dashboard

Q: What about security?

A: This was a demo. Just don’t get hacked, ok?