Cloud Run currently (August 2019) is a beta service from Google, and is a managed compute platform that automatically scales stateless Docker Containers.
I like that Cloud Run is language agnostic - it just needs a container image - all of the server infrastructure is provided for you by Google. As long as the container conforms to the (very simple and logical) Cloud Run container contract then you’re ready to deploy your code as either serverless or on your own GKE cluster - very cool.
We can then build and run it using the Golang command line tools
$ go build helloworld.go
So we have a working program, lets look at our files
$ls-lh hello*-rwxr-xr-x 1 jm staff 2.0M 29 Jul 20:11 helloworld
-rw-r--r-- 1 jm staff 72B 29 Jul 20:04 helloworld.go
Golang has produced a single binary of 2MB from the 72 bytes of source code. One of the great things about Golang is that a single binary is produced. That’s all good, but of course we want the hello world as a docker image.Here is the simple docker file
Now lets build the dockerfile using the command docker build.
and check the size of the image using the command docker images
The container is now ready for use, and we can see that the golang-alpine images is shown to, lets test it our freshly created container by running container 2b
It appears to be working fine, but 354MB seems to be a bit heavy, whats the reason for this?
The base image golang:alpine is a good image for building Golang programs, but we don’t need any of the build tools at runtime. There is a technique called Multi-stage Docker builds which is well explained here. The basic premise is that after building the program using say the golang:alpine image, that you copy the binary and the minimum files that are required for the application into a minimal container for deployment.
This has the following advantages:
The images are smaller, so therefore less disk space is required
Images should load and startup faster when deployed (important for orchestrated or serverless deployment methods)
There are no extra binaries included in the final image, reducing the attack surface, and improving security
Let’s look at the dockerfile for a two stage build
FROM golang:1.12-alpine@sha256:1121c345b1489bb5e8a9a65b612c8fed53c175ce72ac1c76cf12bbfc35211310 as builder# Force the go compiler to use modulesENV GO111MODULE=on# Create the user and group files to run unprivileged RUN mkdir /user &&\
echo'nobody:x:65534:65534:nobody:/:'> /user/passwd &&\
RUN apk update && apk add --no-cache git ca-certificates tzdata
RUN mkdir /build
COPY . /build/WORKDIR /build # Import the codeCOPY ./ ./RUN CGO_ENABLED=0 GOOS=linux go build -a-installsuffix cgo -ldflags'-extldflags "-static"'-o helloworld .
FROM scratch AS finalLABEL author="John Middleton"# Import the time zone filesCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo# Import the user and group filesCOPY --from=builder /user/group /user/passwd /etc/# Import the CA certsCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/# Import the compiled go executableCOPY --from=builder /build/helloworld /WORKDIR /# Run as unprivelegedUSER nobody:nobodyENTRYPOINT ["/helloworld"]# expose portEXPOSE 8080
There is lots to see here, and explain, but lets build the container, and see what happens.
Lets see the size of the container
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 468bbdfc5f9a 3 minutes ago 3.4MB
That’s impressive, our container has “shrunk” to only 3.4MB !
$ docker run 468
And it works!
The Dockerfile is now built from a scratch container, with only the minimum necessary binaries, and running in its own restricted-permissions user namespace
This is a trivial hello world example, but in a future posts, I’ll show what the results were with a simple REST API I’ve been working on, and also serverless deployment using Google Cloud Run.