Comment créer mon site Web personnel à l'aide de conteneurs avec un Makefile
Simplifiez la gestion des conteneurs en combinant les commandes pour créer, tester et déployer un projet dans un Makefile.
L'utilitaire make
et son Makefile associé sont utilisés depuis longtemps pour créer des logiciels. Le Makefile définit un ensemble de commandes à exécuter et l'utilitaire make
les exécute. Il est similaire à un Dockerfile ou à un Containerfile, un ensemble de commandes utilisées pour créer des images de conteneur.
Ensemble, un Makefile et un Containerfile constituent un excellent moyen de gérer un projet basé sur un conteneur. Le Containerfile décrit le contenu de l'image du conteneur et le Makefile décrit comment gérer le projet lui-même : lancer la création de l'image, les tests et le déploiement, entre autres commandes utiles.
Fixer des objectifs
Le Makefile est constitué de « cibles » : une ou plusieurs commandes regroupées sous une seule commande. Vous pouvez exécuter chaque cible en exécutant la commande make
suivie de la cible que vous souhaitez exécuter :
# Runs the "build_image" make target from the Makefile
$ make build_image
C'est la beauté du Makefile. Vous pouvez créer une collection de cibles pour chaque tâche qui doit être effectuée manuellement. Dans le contexte d'un projet basé sur un conteneur, cela inclut la création de l'image, sa transmission vers un registre, le test de l'image, et même le déploiement de l'image et la mise à jour du service qui l'exécute. J'utilise un Makefile pour mon site Web personnel pour effectuer toutes ces tâches de manière simple et automatisée.
Construire, tester, déployer
Je crée mon site Web à l'aide de Hugo, un générateur de site Web statique qui crée du HTML statique à partir de fichiers YAML. J'utilise Hugo pour créer les fichiers HTML pour moi, puis je crée une image de conteneur avec ces fichiers et Caddy, un serveur Web simple et rapide, et j'exécute cette image en tant que conteneur. (Hugo et Caddy sont tous deux des projets open source sous licence Apache.) J'utilise un Makefile pour faciliter la création et le déploiement de cette image en production.
La première cible du Makefile est à juste titre la commande image_build
:
image_build:
podman build --format docker -f Containerfile -t $(IMAGE_REF):$(HASH) .
Cette cible appelle Podman pour créer une image à partir du Containerfile inclus dans le projet. Il y a quelques variables dans la commande ci-dessus : quelles sont-elles ? Les variables peuvent être spécifiées dans le Makefile, de la même manière que Bash ou un langage de programmation. Je les utilise pour diverses choses dans le Makefile, mais le plus utile est de créer la référence d'image à transmettre aux registres d'images de conteneurs distants :
# 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)
À l'aide de ces variables, la cible image_build
crée une référence d'image telle que us.gcr.io/my-project-name/my-image-name:abc1234
en utilisant la courte révision de Git. hash comme balise d'image afin qu'elle puisse être liée facilement au code qui l'a construit.
Le Makefile marque ensuite cette image comme :latest
. Je n'utilise généralement pas :latest
pour quoi que ce soit en production, mais plus loin dans ce Makefile, cela sera utile pour le nettoyage :
image_tag:
podman tag $(IMAGE_REF):$(HASH) $(IMAGE_REF):latest
L'image a donc maintenant été créée et doit être validée pour s'assurer qu'elle répond à certaines exigences minimales. Pour mon site Web personnel, il s'agit simplement de « le serveur Web démarre-t-il et renvoie-t-il quelque chose ? » Cela pourrait être accompli avec des commandes shell dans le Makefile, mais il était plus facile pour moi d'écrire un script Python qui démarre un conteneur avec Podman, envoie une requête HTTP au conteneur, vérifie qu'il reçoit une réponse, puis nettoie le conteneur. La gestion des exceptions "essayer, sauf, enfin" de Python est parfaite pour cela et considérablement plus facile que de répliquer la même logique à partir de commandes shell dans un 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', action='store', required=True, help='image name')
args = parser.parse_args()
print(args.image)
try:
check_call("podman rm smk".split())
except CalledProcessError as err:
pass
check_call(
"podman run --rm --name=smk -p 8080:8080 -d {}".format(args.image).split()
)
time.sleep(5)
r = Request("http://localhost:8080", headers={'Host': 'chris.collins.is'})
try:
print(str(urlopen(r).read()))
finally:
check_call("podman kill smk".split())
Cela pourrait être un test plus approfondi. Par exemple, pendant le processus de génération, le hachage de révision Git pourrait être intégré à la réponse et le test pourrait vérifier que la réponse incluait le hachage attendu. Cela aurait l’avantage de vérifier qu’au moins une partie du contenu attendu est là.
Si tout se passe bien avec les tests, alors l'image est prête à être déployée. J'utilise le service Cloud Run de Google pour héberger mon site Web et, comme tous les principaux services cloud, il existe un excellent outil d'interface de ligne de commande (CLI) que je peux utiliser pour interagir avec le service. Étant donné que Cloud Run est un service de conteneur, le déploiement consiste à transférer les images créées localement vers un registre de conteneurs distant, puis à lancer le déploiement du service à l'aide de l'outil CLI gcloud
.
Vous pouvez effectuer le push en utilisant Podman ou Skopeo (ou Docker, si vous l'utilisez). Ma cible push pousse l'image $ (IMAGE_REF):$ (HASH)
ainsi que la balise :latest
:
push:
podman push --remove-signatures $(IMAGE_REF):$(HASH)
podman push --remove-signatures $(IMAGE_REF):latest
Une fois l'image transférée, utilisez la commande gcloud run déployer
pour déployer l'image la plus récente dans le projet et la rendre active. Encore une fois, le Makefile est utile ici. Je peux spécifier les arguments --platform
et --region
comme variables dans le Makefile afin de ne pas avoir à m'en souvenir à chaque fois. Soyons honnêtes : j'écris si rarement pour mon blog personnel qu'il n'y a aucune chance que je me souvienne de ces variables si je devais les saisir de mémoire à chaque fois que je déployais une nouvelle image :
rollout:
gcloud run deploy $(PROJECT) --image $(IMAGE_REF):$(HASH) --platform $(PLATFORM) --region $(REGION)
Plus de cibles
Il existe des cibles make
supplémentaires utiles. Lorsque j'écris de nouveaux éléments ou que je teste des modifications CSS ou du code, j'aime voir sur quoi je travaille localement sans le déployer sur un serveur distant. Pour cela, mon Makefile dispose d'une commande run_local
, qui lance un conteneur avec le contenu de mon commit actuel et ouvre mon navigateur à l'URL de la page hébergée par le serveur Web exécuté localement :
.PHONY: run_local
run_local:
podman stop 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)
J'utilise également une variable pour le nom du navigateur, afin de pouvoir tester avec plusieurs si je le souhaite. Par défaut, il s'ouvrira dans Firefox lorsque j'exécuterai make run_local
. Si je veux tester la même chose dans Google, je lance make run_local BROWSER="google-chrome"
.
Lorsque vous travaillez avec des conteneurs et des images de conteneurs, nettoyer les anciens conteneurs et images est une corvée ennuyeuse, surtout lorsque vous effectuez fréquemment des itérations. J'inclus également des cibles dans mon Makefile pour gérer ces tâches. Lors du nettoyage d'un conteneur, si le conteneur n'existe pas, Podman ou Docker reviendra avec un code de sortie de 125. Malheureusement, make
s'attend à ce que chaque commande renvoie 0 sinon elle arrêtera le traitement, donc je utilisez un script wrapper pour gérer ce cas :
#!/usr/bin/env bash
ID="${@}"
podman stop ${ID} 2>/dev/null
if [[ $? == 125 ]]
then
# No such container
exit 0
elif [[ $? == 0 ]]
then
podman rm ${ID} 2>/dev/null
else
exit $?
fi
Le nettoyage des images nécessite un peu plus de logique, mais tout peut être effectué dans le Makefile. Pour ce faire facilement, j'ajoute une étiquette (via le Containerfile) à l'image lors de sa construction. Cela permet de retrouver facilement toutes les images portant ces étiquettes. La plus récente de ces images peut être identifiée en recherchant la balise :latest
. Enfin, toutes les images, à l'exception de celles pointant vers l'image taguée avec :latest
, peuvent être supprimées :
clean_images:
$(eval LATEST_IMAGES := $(shell podman images --filter "label=my-project.purpose=app-image" --no-trunc | awk '/latest/ {print $$3}'))
podman images --filter "label=my-project.purpose=app-image" --no-trunc --quiet | grep -v $(LATEST_IMAGES) | xargs --no-run-if-empty --max-lines=1 podman image rm
C'est à ce moment-là que l'utilisation d'un Makefile pour gérer des projets de conteneurs se transforme vraiment en quelque chose de cool. À ce stade, le Makefile comprend des commandes pour créer et marquer des images, tester, pousser des images, déployer une nouvelle version, nettoyer un conteneur, nettoyer des images et exécuter une version locale. Exécuter chacune d'elles avec make image_build && make image_tag && make test
… etc. est considérablement plus facile que d'exécuter chacune des commandes d'origine, mais cela peut être encore plus simplifié.
Un Makefile peut regrouper des commandes dans une cible, permettant à plusieurs cibles de s'exécuter avec une seule commande. Par exemple, mon Makefile regroupe les cibles image_build
et image_tag
sous la cible build
, donc je peux exécuter les deux en utilisant simplement make build
. Mieux encore, ces cibles peuvent être regroupées dans la cible make
par défaut, all
, me permettant de toutes les exécuter dans l'ordre en exécutant make all
ou plus simplement, make
.
Pour mon projet, je souhaite que l'action make
par défaut inclue tout, de la création de l'image au test, au déploiement et au nettoyage, j'inclus donc les cibles suivantes :
.PHONY: all
all: build test deploy clean
.PHONY: build image_build image_tag
build: image_build image_tag
.PHONY: deploy push rollout
deploy: push rollout
.PHONY: clean clean_containers clean_images
clean: clean_containers clean_images
Cela fait tout ce dont j'ai parlé dans cet article, à l'exception de la cible make run_local
, en une seule commande : make
.
Conclusion
Un Makefile est un excellent moyen de gérer un projet basé sur un conteneur. En combinant toutes les commandes nécessaires pour créer, tester et déployer un projet dans des cibles make
au sein du Makefile, tout le travail « méta » (tout en dehors de l'écriture du code) peut être simplifié et automatisé. Le Makefile peut même être utilisé pour des tâches liées au code : exécuter des tests unitaires, maintenir des modules, compiler des binaires et des sommes de contrôle. Bien qu'il ne puisse pas encore écrire de code pour vous, l'utilisation d'un Makefile combinée aux avantages d'un service conteneurisé basé sur le cloud peut faciliter
(wink, wink) la gestion de nombreux aspects d'un projet beaucoup plus facilement.