Containerized Tooling

In day-to-day work we constantly use various tools: protoc, golangci-lint, allure, and many others.

To avoid situations where one developer’s tool version or configuration differs from another’s — or from what’s set up in CI — our tools are “baked” into containers.

The tooling containers are built in CI and are available both on local development and debugging machines and in CI pipelines. That gives us a unified configuration and end-to-end versioning for every tool across every environment.

We took a systematic approach to containerizing our tooling:

All repositories share a single common pipeline for building and publishing to the internal image registry.

A project group might look like this:

tree -a -C -L 2 tooling
tooling
├── golangci-lint
│   ├── .env
│   ├── .git
│   ├── Dockerfile
│   ├── Makefile
│   ├── README.md
│   ├── entrypoint.sh
│   └── golangci.yaml
├── protoc
│   ├── .env
│   ├── .git
│   ├── Dockerfile
│   ├── Makefile
│   ├── README.md
...

The .env file is responsible for configuration; it usually sets the image name:

IMAGE_NAME=internal-registry.dmz/dx/tooling/cowsay

Every repository ships with a templated Makefile that may differ slightly from the others when extra steps are needed. The .env file is included in the very first line of the Makefile:

include .env

.DEFAULT_GOAL := build

.PHONY: build
build:
	docker build --tag ${IMAGE_NAME} .

The Dockerfile doesn’t really need a separate description; here’s the simplest possible example:

# syntax=docker/dockerfile:1

FROM ubuntu

RUN <<EOF
apt-get update
apt-get upgrade -y

apt-get install -y cowsay
EOF

WORKDIR /workdir

ENTRYPOINT ["/usr/games/cowsay"]

Two things are worth pointing out that may not be obvious:

Thanks to this approach to containerizing tools, my team gets:

References