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 cannotADD ../something /something
, because the first step of adocker 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.
Eventually you’ll get a project built.
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.
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-171905
to 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?