Recently I wrote on Twitter about how doing CI right requires you to properly separate your build and run steps for your container images.
i.e. you have one Docker image to build your Go static binary, and one that's 3 lines long to copy it into a scratch image ;)— Ian Lewis (@IanMLewis) May 20, 2017
The reason for this issue is that you want to keep your final image as small as possible for a number of reasons. The obvious reason is for performance but there are several other reasons. Keeping it small keeps the images simple and reduces the risk of bugs, and reduces the security attack surface.
If you’re a Google Cloud junkie like me then Container Builder is really convenient because it integrates really well with Google Cloud Source Repositories and Google Container Registry. But best of all, it makes it easy to separate out our build steps.
Container Builder is a CI/CD tool that let’s you kick off build steps manually or based on triggers. So you can kick off builds when you push to your source repo. In this post I’m just going to kick them off manually, but you can read the docs to set up triggers.
Container Builder runs build steps in the order you specify but those build steps can be any Docker image. So it’s extremely flexible. There are a bunch of “built-in” supported images you can use (they are also open source!). Custom build step images are also pretty easy to create.
Kicking off a build from a simple Dockerfile is easy. This gcloud command will build your Dockerfile and store it with the GCR image name you specify.
gcloud container builds submit --tag gcr.io/[PROJECT-ID]/[IMAGE] .
However, that will build the Docker image in one step. To do more complicated builds you can use a cloudbuild.yaml to define the steps. That way you can build your Go app, run tests, and finally build the image, and push it.
There is a
gcr.io/cloud-builders/go image that you can use to run the go compiler. Here is a cloudbuild.yaml for a app that I built. Based on the docs for the image, I specify the
PROJECT_ROOT environment variable so that the image will link my project into the
GOPATH. From there I can run any Go command. I can run go generate, go test, and finally go install to build the binary.
steps: - name: "gcr.io/cloud-builders/go" args: ["generate"] env: ["PROJECT_ROOT=github.com/IanLewis/testapp"] - name: "gcr.io/cloud-builders/go" args: ["test", "./..."] env: ["PROJECT_ROOT=github.com/IanLewis/testapp"] - name: "gcr.io/cloud-builders/go" args: [ "install", "-a", "-ldflags", "'-s'", "-installsuffix", "cgo", "github.com/IanLewis/testapp", ] env: [ "PROJECT_ROOT=github.com/IanLewis/testapp", "CGO_ENABLED=0", "GOOS=linux", ]
In the final step I build the Docker image.
- name: 'gcr.io/cloud-builders/docker' args: ['build', '--tag=gcr.io/$PROJECT_ID/testapp', '.'] images: ['gcr.io/$PROJECT_ID/testapp']
Here is the Dockerfile. go install puts my binary in gopath/bin so I can copy it into my image from there.
FROM scratch COPY gopath/bin/testapp /testapp CMD ["/testapp"]
You can submit a build with a cloudbuild.yaml like so:
gcloud container builds submit --config cloudbuild.yaml
This will build us a nice small image that we can pull from