Ricerca nel sito web

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.

Articoli correlati: