The principle purpose of this blog post is to serve as a first post for my website. Some of what I’m doing here may seem a bit vague as this is a quick an dirty explanation to use while I test my blog post model. I’d like this to be more thorough but I need to start somewhere.
With that out of the way, let’s get to it.
What/who is this for? #
I’ve often found that I’m working on projects where I need to create a small service.
- This might be just a single Docker container developed locally
- Or a larger project developed locally using multiple containers and compose.
- Almost always the app is built locally and pushed to a registry. No CICD for these. (but you could)
- Deployed to either a Docker host (i.e. special exporters) but most often Kuberentes.
A sample application #
I don’t have very many good public examples of this but an older version of this concept can be found here: https://github.com/tolson-vkn/env-echo/blob/master/Makefile
When you first enter this application you can run make
:
$ make
+------------------+
| env-echo targets |
+------------------+
build Build env-echo locally
dev Start dev env-echo container
shell Start a /bin/bash shell on env-echo container
up Start immutable env-echo container
exec Exec /bin/bash into running env-echo
watch Start a watch command
login Log into registry
publish Build locally and publish to registry
deploy Deploy env-echo to Kuberentes
You’re presented with some helpful command to get you started. I always make help text be the first thing. When using these Makefiles we aren’t building something in C, what we might use make for could be in doubt.
Before we get into the output let’s look at the start of the Makefile
TAG=$(shell git log --pretty=format:'%h' -n 1)
REGISTRY=timmyolson/env-echo
PROVIDER=docker.io
MAKE_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
Here are some helpers defined. The first, tag is our image tag. For this repo we aren’t using semantic versioning. The command uses git log to to turn a commit into 90c376c
. We will use this when we push our images to the registry.
MAKE_DIR
is also worth pointing out, docker likes full paths when run from the CLI and using volume mounts; this is not ok -v ~/mydir:/opt
so MAKE_DIR
gives us a nice way to get the absolute path.
Ok back to the make output: The help is generated from this beauty of shell
@grep -E '^[a-zA-Z\-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'
Clearly this build our table but how do we consume it?
.PHONY: publish
publish: ## Build locally and publish to registry
In the example for pushing our images to our registry, clearly publish is left most, but whenever we see a recipe and ##
we take the recipe name and whatever is rightmost ##
. If you want hidden recipes or helpers, just leave it out.
A larger example:
.PHONY: dev
dev: ## Start dev env-echo container
docker run --rm -i -t \
--name env-echo \
-v $(MAKE_DIR)/flask/app:/app \
-v $(MAKE_DIR)/flask/requirements.txt:/requirements.txt \
-e DEBUG=True \
-p 5000:5000 \
env-echo
This is what we will use to mount our code into the container and start the app. Like normal CLI we’re ready to hack. The Makefile is valuable because we don’t have to remember the command to run.
I can already hear you saying; “What’s the value this delivers that docker-compose
doesn’t have?”
At the surface it appears there is no advantage. And I will concede that larger projects end up looking like:
.PHONY: dev
dev: ## Start dev env-echo container
docker-compose up
Where there is power is that;
- What if you had multiple `docker-compose` environments a single `make dev` can find them all through make hierarchy. Think git submodules that container compose for the frontend, backend. We can bring it up easy with `make dev`
- Sometimes `docker-compose`'s naming schemes around containers and volumes for small projects is cumbersome
- What if you want to daemonize `docker-compose` but foreground logs
docker-compose up -d
docker-compose logs -f
Other examples #
Anyways back to other recipes. Sometimes it doesn’t pay to use CICD tooling for applications. My toolbox image is a perfect example. So the make publish
works well.
.PHONY: publish
publish: ## Build locally and publish to registry
@printf '\033[33mBuild and Push ubuntools\033[0m\n';
docker build -t ubuntools:$(TAG) ./flask
docker tag ubuntools:$(TAG) $(REGISTRY):$(TAG)
docker push $(REGISTRY):$(TAG)
@printf '\033[33mUpdate the latest tag to $(TAG)\033[0m\n';
docker tag ubuntools:$(TAG) $(REGISTRY):latest
docker push $(REGISTRY):latest
As a final example. Not all of our development work with docker is just starting and stopping containers. Let’s say you wanted to have a handy way to connect to a containers postgres, but you don’t know the schema, etc.
.PHONY: psql
psql: ## Connect host psql to running container
ifeq ($(shell which psql &> /dev/null && echo installed), installed)
psql -U postgres -h localhost -p 5432 db
else
@printf '\033[31mError: psql not installed locally!\033[0m\n'
endif
I added a bit of flair here, this one has something different. I’d trust that users of my repo have docker installed but they might night have the psql
binary. So I check it with ifeq
. This is quite simply a string comparison tool in make
that is rather primitive. We run the command which psql &> /dev/null && echo installed
which if we don’t have the binary it short circuits and the condition is false, or if the string is installed; true.
In summary, I’m not a madman, no I don’t do this all the time. I understand there are times for sensible tools like docker-compose
. But sometimes when I need a basic repo for a small app or a hacking project, I really enjoy using make
to drive the environment. Hopefully you learned something.