The make
utility and its associated Makefile have been used to construct software program for a very long time. The Makefile defines a set of instructions to run, and the make
utility runs them. It is much like a Dockerfile or Containerfile—a set of instructions used to construct container photos.
Together, a Makefile and Containerfile are a superb option to handle a container-based mission. The Containerfile describes the contents of the container picture, and the Makefile describes how one can handle the mission itself: kicking the picture construct, testing, and deployment, amongst different useful instructions.
Make targets
The Makefile consists of “targets”: a number of instructions grouped below a single command. You can run every goal by operating the make
command adopted by the goal you wish to run:
# Runs the "build_image" make goal from the Makefile
$ make build_image
This is the great thing about the Makefile. You can construct a set of targets for every process that must be carried out manually. In the context of a container-based mission, this contains constructing the picture, pushing it to a registry, testing the picture, and even deploying the picture and updating the service operating it. I take advantage of a Makefile for my private web site to do all these duties in a simple, automated means.
Build, check, deploy
I construct my web site utilizing Hugo, a static web site generator that builds static HTML from YAML recordsdata. I take advantage of Hugo to construct the HTML recordsdata for me, then construct a container picture with these recordsdata and Caddy, a quick and easy internet server, and run that picture as a container. (Both Hugo and Caddy are open supply, Apache-licensed tasks.) I take advantage of a Makefile to make constructing and deploying that picture to manufacturing a lot simpler.
The first goal within the Makefile is appropriately the image_build
command:
image_build:
podman construct --format docker -f Containerfile -t $(IMAGE_REF):$(HASH) .
This goal invokes Podman to construct a picture from the Containerfile included within the mission. There are some variables within the command above—what are they? Variables may be specified within the Makefile, equally to Bash or a programming language. I take advantage of them for a wide range of issues inside the Makefile, however essentially the most helpful is constructing the picture reference to be pushed to distant container picture registries:
# Image values
REGISTRY := "us.gcr.io"
PROJECT := "my-project-name"
IMAGE := "some-image-name"
IMAGE_REF := $(REGISTRY)/$(PROJECT)/$(IMAGE)# Git commit hash
HASH := $(shell git rev-parse --short HEAD)
Using these variables, the image_build
goal builds a picture reference like us.gcr.io/my-project-name/my-image-name:abc1234
utilizing the quick Git revision hash because the picture tag in order that it may be tied to the code that constructed it simply.
The Makefile then tags that picture as :newest
. I do not usually use :newest
for something in manufacturing, however additional down on this Makefile, it can are available in helpful for cleanup:
image_tag:
podman tag $(IMAGE_REF):$(HASH) $(IMAGE_REF):newest
So, now the picture has been constructed and must be validated to ensure it meets some minimal necessities. For my private web site, that is truthfully simply, “does the webserver start and return something?” This could possibly be completed with shell instructions within the Makefile, but it surely was simpler for me to write down a Python script that begins a container with Podman, points an HTTP request to the container, verifies it receives a reply, after which cleans up the container. Python’s “try, except, finally” exception dealing with is ideal for this and significantly simpler than replicating the identical logic from shell instructions in a Makefile:
#!/usr/bin/env python3import time
import argparse
from subprocess import check_call, CalledProcessError
from urllib.request import urlopen, Requestparser = argparse.ArgumentParser()
parser.add_argument('-i', '--image', motion='retailer', required=True, assist='picture title')
args = parser.parse_args()print(args.picture)
attempt:
check_call("podman rm smk".break up())
besides CalledProcessError as err:
movecheck_call(
"podman run --rm --name=smk -p 8080:8080 -d ".format(args.picture).break up()
)time.sleep(5)
r = Request("http://localhost:8080", headers=)
attempt:
print(str(urlopen(r).learn()))
lastly:
check_call("podman kill smk".break up())
This could possibly be a extra thorough check. For instance, in the course of the construct course of, the Git revision hash could possibly be constructed into the response, and the check may verify that the response included the anticipated hash. This would take pleasure in verifying that a minimum of a few of the anticipated content material is there.
If all goes effectively with the assessments, then the picture is able to be deployed. I take advantage of Google’s Cloud Run service to host my web site, and like every of the key cloud providers, there is a wonderful command-line interface (CLI) software that I can use to work together with the service. Since Cloud Run is a container service, deployment consists of pushing the photographs constructed domestically to a distant container registry, after which kicking off a rollout of the service utilizing the gcloud
CLI software.
You can do the push utilizing Podman or Skopeo (or Docker, should you’re utilizing it). My push goal pushes the $(IMAGE_REF):$(HASH)
picture and likewise the :newest
tag:
push:
podman push --remove-signatures $(IMAGE_REF):$(HASH)
podman push --remove-signatures $(IMAGE_REF):newest
After the picture has been pushed, use the gcloud run deploy
command to roll out the latest picture to the mission and make the brand new picture reside. Once once more, the Makefile turns out to be useful right here. I can specify the --platform
and --region
arguments as variables within the Makefile in order that I haven’t got to recollect them every time. Let’s be trustworthy: I write so sometimes for my private weblog, there may be zero likelihood I might bear in mind these variables if I needed to sort them from reminiscence every time I deployed a brand new picture:
rollout:
gcloud run deploy $(PROJECT) --image $(IMAGE_REF):$(HASH) --platform $(PLATFORM) --region $(REGION)
More targets
There are extra useful make
targets. When writing new stuff or testing CSS or code modifications, I wish to see what I am engaged on domestically with out deploying it to a distant server. For this, my Makefile has a run_local
command, which spins up a container with the contents of my present commit and opens my browser to the URL of the web page hosted by the domestically operating webserver:
.PHONY: run_local
run_local:
podman cease mansmk ; podman rm mansmk ; podman run --name=mansmk --rm -p $(HOST_ADDR):$(HOST_PORT):$(TARGET_PORT) -d $(IMAGE_REF):$(HASH) && $(BROWSER) $(HOST_URL):$(HOST_PORT)
I additionally use a variable for the browser title, so I can check with a number of if I wish to. By default, it can open in Firefox after I run make run_local
. If I wish to check the identical factor in Google, I run make run_local BROWSER="google-chrome"
.
When working with containers and container photos, cleansing up previous containers and pictures is an annoying chore, particularly while you iterate incessantly. I embrace targets in my Makefile for dealing with these duties, too. When cleansing up a container, if the container would not exist, Podman or Docker will return with an exit code of 125. Unfortunately, make
expects every command to return zero or it can cease processing, so I take advantage of a wrapper script to deal with that case:
#!/usr/bin/env bashID="$"
podman cease $ 2>/dev/null
if [[ $? == 125 ]]
then
# No such container
exit zero
elif [[ $? == 0 ]]
then
podman rm $ 2>/dev/null
else
exit $?
fi
Cleaning photos requires a bit extra logic, however it might probably all be achieved inside the Makefile. To do that simply, I add a label (through the Containerfile) to the picture when it is being constructed. This makes it simple to search out all the photographs with these labels. The most up-to-date of those photos may be recognized by in search of the :newest
tag. Finally, all the photos, besides these pointing to the picture tagged with :newest
, may be deleted:
clean_images:
$(eval LATEST_IMAGES := $(shell podman photos --filter "label=my-project.purpose=app-image" --no-trunc | awk '/newest/ print $$three'))
podman photos --filter "label=my-project.purpose=app-image" --no-trunc --quiet | grep -v $(LATEST_IMAGES) | xargs --no-run-if-empty --max-lines=1 podman picture rm
This is the purpose the place utilizing a Makefile for managing container tasks actually comes collectively into one thing cool. To this level, the Makefile contains instructions for constructing and tagging photos, testing, pushing photos, rolling out a brand new model, cleansing up a container, cleansing up photos, and operating a neighborhood model. Running every of those with make image_build && make image_tag && make check
… and so forth. is significantly simpler than operating every of the unique instructions, however it may be simplified even additional.
A Makefile can group instructions right into a goal, permitting a number of targets to run with a single command. For instance, my Makefile teams the image_build
and image_tag
targets below the construct
goal, so I can run each by merely utilizing make construct
. Even higher, these targets may be additional grouped into the default make
goal, all
, permitting me to run all of them so as by executing make all
or extra merely, make
.
For my mission, I would like the default make
motion to incorporate every part from constructing the picture to testing, deploying, and cleansing up, so I embrace the next targets:
.PHONY: all
all: construct check deploy clear.PHONY: construct image_build image_tag
construct: image_build image_tag.PHONY: deploy push rollout
deploy: push rollout.PHONY: clear clean_containers clean_images
clear: clean_containers clean_images
This does every part I’ve talked about on this article, besides the make run_local
goal, in a single command: make
.
Conclusion
A Makefile is a wonderful option to handle a container-based mission. By combining all of the instructions needed to construct, check, and deploy a mission into make
targets inside the Makefile, all of the “meta” work—every part other than writing the code—may be simplified and automatic. The Makefile may even be used for code-related duties: operating unit assessments, sustaining modules, compiling binaries and checksums. While it might probably’t but write code for you, utilizing a Makefile mixed with the advantages of a containerized, cloud-based service can make
(wink, wink) managing many facets of a mission a lot simpler.