Science and technology

How I construct my private web site utilizing containers with a Makefile

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 python3

import time
import argparse
from subprocess import check_call, CalledProcessError
from urllib.request import urlopen, Request

parser = 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:
    move

check_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 bash

ID="$"

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.

Most Popular

To Top