Come creo il mio sito web personale utilizzando contenitori con un Makefile
Semplifica la gestione dei contenitori combinando i comandi per creare, testare e distribuire un progetto in un Makefile.
L'utilità make
e il relativo Makefile sono stati utilizzati per creare software per molto tempo. Il Makefile definisce un insieme di comandi da eseguire e l'utility make
li esegue. È simile a un Dockerfile o Containerfile: un insieme di comandi utilizzati per creare immagini del contenitore.
Insieme, Makefile e Containerfile sono un modo eccellente per gestire un progetto basato su container. Il Containerfile descrive il contenuto dell'immagine del contenitore e il Makefile descrive come gestire il progetto stesso: avviare la creazione dell'immagine, testare e distribuire, tra gli altri comandi utili.
Stabilisci obiettivi
Il Makefile è costituito da "obiettivi": uno o più comandi raggruppati sotto un unico comando. Puoi eseguire ciascun target eseguendo il comando make
seguito dal target che desideri eseguire:
# Runs the "build_image" make target from the Makefile
$ make build_image
Questa è la bellezza del Makefile. È possibile creare una raccolta di obiettivi per ogni attività che deve essere eseguita manualmente. Nel contesto di un progetto basato su container, ciò include la creazione dell'immagine, il suo push in un registro, il test dell'immagine e persino la distribuzione dell'immagine e l'aggiornamento del servizio che la esegue. Utilizzo un Makefile per il mio sito web personale per svolgere tutte queste attività in modo semplice e automatizzato.
Costruisci, testa, distribuisci
Costruisco il mio sito Web utilizzando Hugo, un generatore di siti Web statici che crea HTML statico da file YAML. Utilizzo Hugo per creare i file HTML per me, quindi creo un'immagine del contenitore con quei file e Caddy, un server Web veloce e semplice, ed eseguo quell'immagine come contenitore. (Sia Hugo che Caddy sono progetti open source con licenza Apache.) Utilizzo un Makefile per rendere molto più semplice la creazione e la distribuzione dell'immagine in produzione.
Il primo obiettivo nel Makefile è appropriatamente il comando image_build
:
image_build:
podman build --format docker -f Containerfile -t $(IMAGE_REF):$(HASH) .
Questo target richiama Podman per creare un'immagine dal Containerfile incluso nel progetto. Ci sono alcune variabili nel comando sopra: cosa sono? Le variabili possono essere specificate nel Makefile, in modo simile a Bash o ad un linguaggio di programmazione. Li utilizzo per una varietà di cose all'interno del Makefile, ma la più utile è creare il riferimento all'immagine da inviare ai registri di immagini del contenitore remoto:
# 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)
Utilizzando queste variabili, il target image_build
crea un riferimento all'immagine come us.gcr.io/my-project-name/my-image-name:abc1234
utilizzando la breve revisione Git hash come tag immagine in modo che possa essere collegato facilmente al codice che lo ha creato.
Il Makefile quindi tagga l'immagine come :latest
. Generalmente non utilizzo :latest
per qualsiasi cosa in produzione, ma più avanti in questo Makefile, tornerà utile per la pulizia:
image_tag:
podman tag $(IMAGE_REF):$(HASH) $(IMAGE_REF):latest
Quindi, ora l'immagine è stata creata e deve essere convalidata per assicurarsi che soddisfi alcuni requisiti minimi. Per il mio sito web personale, onestamente è solo: "il server web si avvia e restituisce qualcosa?" Ciò potrebbe essere ottenuto con i comandi shell nel Makefile, ma per me è stato più semplice scrivere uno script Python che avvia un contenitore con Podman, invia una richiesta HTTP al contenitore, verifica che riceva una risposta e quindi ripulisce il contenitore. La gestione delle eccezioni "prova, tranne, finalmente" di Python è perfetta per questo e notevolmente più semplice che replicare la stessa logica dai comandi shell in 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())
Questo potrebbe essere un test più approfondito. Ad esempio, durante il processo di compilazione, l'hash di revisione Git potrebbe essere integrato nella risposta e il test potrebbe verificare che la risposta includa l'hash previsto. Ciò avrebbe il vantaggio di verificare che almeno parte del contenuto previsto sia presente.
Se tutto va bene con i test, l'immagine è pronta per essere distribuita. Utilizzo il servizio Cloud Run di Google per ospitare il mio sito Web e, come tutti i principali servizi cloud, esiste un eccellente strumento CLI (interfaccia a riga di comando) che posso utilizzare per interagire con il servizio. Poiché Cloud Run è un servizio container, la distribuzione consiste nell'invio delle immagini create localmente a un registro container remoto, quindi nell'avvio dell'implementazione del servizio utilizzando lo strumento CLI gcloud
.
Puoi eseguire il push utilizzando Podman o Skopeo (o Docker, se lo stai utilizzando). Il mio target push inserisce l'immagine $ (IMAGE_REF):$ (HASH)
e anche il tag :latest
:
push:
podman push --remove-signatures $(IMAGE_REF):$(HASH)
podman push --remove-signatures $(IMAGE_REF):latest
Dopo che l'immagine è stata inviata, utilizza il comando gcloud run deploy
per implementare l'immagine più recente nel progetto e rendere attiva la nuova immagine. Ancora una volta, il Makefile torna utile in questo caso. Posso specificare gli argomenti --platform
e --region
come variabili nel Makefile in modo da non doverli ricordare ogni volta. Siamo onesti: scrivo così raramente per il mio blog personale che non ci sono possibilità di ricordare queste variabili se dovessi digitarle dalla memoria ogni volta che distribuisco una nuova immagine:
rollout:
gcloud run deploy $(PROJECT) --image $(IMAGE_REF):$(HASH) --platform $(PLATFORM) --region $(REGION)
Più obiettivi
Esistono ulteriori target make
utili. Quando scrivo cose nuove o testo CSS o modifiche al codice, mi piace vedere su cosa sto lavorando localmente senza distribuirlo su un server remoto. Per questo, il mio Makefile ha un comando run_local
, che avvia un contenitore con il contenuto del mio commit attuale e apre il mio browser all'URL della pagina ospitata dal server web in esecuzione localmente:
.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)
Utilizzo anche una variabile per il nome del browser, quindi posso testarne diversi se lo desidero. Per impostazione predefinita, si aprirà in Firefox quando eseguo make run_local
. Se voglio testare la stessa cosa su Google, eseguo make run_local BROWSER="google-chrome"
.
Quando si lavora con contenitori e immagini di contenitori, ripulire vecchi contenitori e immagini è un compito fastidioso, soprattutto quando si esegue l'iterazione frequentemente. Includo anche obiettivi nel mio Makefile per gestire queste attività. Quando si pulisce un contenitore, se il contenitore non esiste, Podman o Docker restituiranno un codice di uscita pari a 125. Sfortunatamente, make
si aspetta che ogni comando restituisca 0 o interromperà l'elaborazione, quindi usa uno script wrapper per gestire quel caso:
#!/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
La pulizia delle immagini richiede un po' più di logica, ma può essere eseguita tutta all'interno del Makefile. Per farlo facilmente, aggiungo un'etichetta (tramite Containerfile) all'immagine durante la sua creazione. Ciò semplifica la ricerca di tutte le immagini con queste etichette. La più recente di queste immagini può essere identificata cercando il tag :latest
. Infine, tutte le immagini, tranne quelle che puntano all'immagine taggata con :latest
, possono essere eliminate:
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
Questo è il punto in cui l'utilizzo di un Makefile per la gestione di progetti contenitore si trasforma davvero in qualcosa di interessante. A questo punto, il Makefile include comandi per creare e taggare immagini, testare, inviare immagini, implementare una nuova versione, pulire un contenitore, pulire immagini ed eseguire una versione locale. Eseguire ciascuno di questi con make image_build && make image_tag && make test
… ecc. è notevolmente più semplice che eseguire ciascuno dei comandi originali, ma può essere ulteriormente semplificato.
Un Makefile può raggruppare i comandi in un target, consentendo l'esecuzione di più target con un singolo comando. Ad esempio, il mio Makefile raggruppa i target image_build
e image_tag
sotto il target build
, quindi posso eseguirli entrambi semplicemente utilizzando make build . Ancora meglio, questi target possono essere ulteriormente raggruppati nel target
.make
predefinito, all
, permettendomi di eseguirli tutti in ordine eseguendo make all
o più semplicemente, make
Per il mio progetto, desidero che l'azione make
predefinita includa tutto, dalla creazione dell'immagine al test, alla distribuzione e alla pulizia, quindi includo i seguenti obiettivi:
.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
Questo fa tutto ciò di cui ho parlato in questo articolo, tranne il target make run_local
, in un singolo comando: make
.
Conclusione
Un Makefile è un modo eccellente per gestire un progetto basato su contenitori. Combinando tutti i comandi necessari per creare, testare e distribuire un progetto in target make
all'interno del Makefile, tutto il "meta" lavoro, tutto tranne la scrittura del codice, può essere semplificato e automatizzato. Il Makefile può essere utilizzato anche per attività relative al codice: esecuzione di unit test, manutenzione di moduli, compilazione di file binari e checksum. Sebbene non possa ancora scrivere codice per te, l'utilizzo di un Makefile combinato con i vantaggi di un servizio containerizzato e basato su cloud può rendere
(occhiolino, occhiolino) la gestione di molti aspetti di un progetto molto più semplice.