Over this year, I’ve slowly become more Golang oriented as a developer and I’m really enjoying the journey that choice is taking me on.
Traditionally, I’ve built web services with Spring Boot and Java but more recently, I’ve noticed there are a number of benefits to developing such services with Golang and it’s standard library: Lower runtime resource requirements and no runtime environment required to name a few.
Another benefit I’d like to discuss here is achieving tiny containerized applications with Golang.
The main advantages of creating container images which are small in size are that they:
The above points typically keep cloud subscriptions costs down and, in extension, your product team happy 🙂
Another key advantage to stripping down container images to their absolute bare necessities is that it reduces that container’s surface area for exploitation - meaning safer deployments too.
To show how to create slender containers with Golang, we need a sample application to containerize, so I created one: https://github.com/iambenzo/dirtyhttp-example
It’s a simple web service, which exposes an in-memory data structure for the storing and retrieval of user information via HTTP request. (Only around 200 lines of code, including spacing and comments if you want to check it out)
Usage instructions are included in the README file.
In the same way some of us struggle to digest full fat milk, our container registries would eventually become bloated with full fat containers.
Let’s see how much damage a creamy Golang container with all the bells and whistles does:
|
|
The command to build the container locally is:
|
|
Once docker has finished working it’s magic, we’ll have a container that looks like this:
You can see above that our “fat” container image requires a meaty 398MB of storage space.
To run the container image, you’d use the following command:
|
|
The wonderful developers at Google have made a “runtime” container available to eliminate some of the bloat in our previous container.
To use it properly, we need to make use of a multi-stage build. This is where we use one [intermediary] docker image to build our application and send the output binary to a slimmed down image where the application will actually run.
|
|
The output container image in the above scenario is the second [runtime] one, based on gcr.io/distroless/base-debian10 , where the only addition to that image is the application binary which was built in a full-fat Golang image. Let’s build it:
|
|
The “semi” version of our container image is already significantly smaller - 26.6MB! This is just under 15 times smaller than our previous “fat” version.
To run the container image, you’d use the following command:
|
|
We’re at a good size now that we’re utilising Google’s stripped-out runtime base image - but we can go even further.
Because we know our codebase is pure Golang code and doesn’t depend on any C code, we can build a statically linked binary in our stage-one (build) image and use the smallest base image available - scratch - as the base image our stage-two (runtime) image:
|
|
Let’s quickly go over the fancy adjustments to the build step:
First, CGO_ENABLED=0
disables the C interoperability and is the part of the command that means we build a binary that’s been statically linked with any libraries we use.
Next, GOOS=linux
tells the build command that we’re building a binary for the linux platform.
The -a
flag forces the rebuilding of all packages, even if they’re up-to-date.
Finally, the -installsuffix
flag simply separates the current build output from any default build output in the package installation directory.
Now that we’ve covered that off, let’s see how the container image looks:
|
|
We’ve achieved an even slimmer container image - 6.83MB! Our “soya” image is just under 4 times smaller than our semi-skimmed image and a massive 58 times smaller than our full fat image!
To run the container image, you’d use the following command:
|
|
Typically, container registry SKUs place limits on storage space, image upload bandwidth and image download bandwidth. Increasing, or surpassing those limits costs cash money.
With that in mind, it’s our duty as penny-pinching product teams to ensure that our container images are as small as possible.
Taking advantage of multi-stage container builds has a huge impact on container image size - a step that should be taken regardless of programming language.
With Golang, we’ve been able to take the multi-stage build one step further by building a binary that will run on a FROM scratch
base image. I can imagine that this could be applicable to any compiled language that doesn’t need a runtime (C++, Rust, etc).
I know that Spring Native is in beta right now - once it’s out of beta, I’d love to see if comparable image sizes can be built with Java.
With containers this small, it’d be a while before you start paying more for your platform!