Automatizamos el trabajo con contenedores a través del Makefile: compile, pruebe e implemente en una sola llamada

La utilidad make facilita la administración de contenedores al combinar comandos para compilar, probar e implementar en un solo archivo de configuración.





Los desarrolladores han estado utilizando la utilidad make durante muchos años. Cuando se lanza, la utilidad lee un archivo con una descripción del proyecto (Makefile) e interpreta su contenido y toma las acciones necesarias. El archivo de descripción del proyecto es un archivo de configuración de texto que describe las dependencias y los comandos que deben ejecutarse. Es similar a un Dockerfile u otro archivo de configuración de contenedores: también enumera los comandos en función de qué imágenes se forman para implementar contenedores.



En este artículo, explicaré cómo administrar contenedores usando un Makefile. El archivo de configuración del contenedor describe la imagen del contenedor, y el Makefile describe el proceso de construcción del proyecto, las pruebas y la implementación, y otros comandos útiles.



Propósito y estructura del Makefile



La utilidad make se instala de forma predeterminada en la mayoría de las distribuciones modernas de Linux, por lo que normalmente no hay problemas para usarla. Y para comenzar a usarlo, debe crear un archivo llamado Makefile.



Un Makefile consta de un conjunto de destinos, dependencias y comandos necesarios para ejecutarlos:



target: [dependency [dependency [dependency ... [dependency]]]]

	commands

	commands

#	commands

      
      





Una meta es un resultado deseado, la forma de lograrlo se describe en el Makefile. Un objetivo significa realizar una acción o obtener una nueva versión de un archivo. Y la dependencia es una especie de "datos iniciales", condiciones necesarias para lograr un objetivo específico. Una dependencia puede ser el resultado de un destino diferente o de un archivo normal.



Los comandos se ingresan usando el carácter de tabulación (los espacios no funcionan). El destino puede no contener comandos, pero puede contener dependencias.



Primero, todas las dependencias se verifican y ejecutan en orden, si algún comando de la dependencia se completa con un error distinto de cero, la ejecución del comando se interrumpe. Además, todos los comandos enumerados en el objetivo mismo se ejecutan, si algún comando se completa con un error distinto de cero, el objetivo se interrumpe.



Todos los comandos se ejecutan en el contexto del directorio actual. Puede comentar un comando colocando un signo de almohadilla (#) delante de él.



Para enviar un objetivo para su ejecución, debe especificar su nombre al llamar a la utilidad make :



#   "build_image" 

$ make build_image
      
      





Esa es la belleza de un Makefile. Puede crear un conjunto de objetivos para cada tarea. En el contexto de la gestión de contenedores, se trata de crear una imagen y enviarla al registro, probar e implementar un contenedor y actualizar su servicio.



Ilustraré el uso del Makefile usando mi sitio web personal como ejemplo y le mostraré lo fácil que es automatizar estas tareas.



Construya, pruebe e implemente



Creé un sitio web simple usando Hugo , un generador de sitios estáticos. Le permite obtener HTML estático de archivos YAML. Usé Caddy como servidor web .



Ahora veamos cómo el Makefile facilita la creación e implementación de este proyecto para producción.



El primer objetivo en el Makefile será image_build :



image_build:

  podman build --format docker -f Containerfile -t $(IMAGE_REF):$(HASH) .
      
      





Este objetivo llama a Podman para crear un contenedor a partir de su Containerfile incluido en el proyecto. Hay varias variables en el comando anterior. Las variables en un Makefile se pueden usar de la misma manera que en los lenguajes de scripting simples. En este caso, necesito esto para crear un "enlace" a la imagen que se enviará al registro 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)
      
      





Usando estas variables, el objetivo image_build genera un identificador de la forma us.gcr.io/my-project-name/my-image-name:abc1234.



Finalmente, el hash de la revisión de Git correspondiente se agrega como una etiqueta de imagen.



En nuestro caso, la imagen se etiquetará con la etiqueta : latest . Esta etiqueta es útil para limpiar el contenedor:



image_tag:

  podman tag $(IMAGE_REF):$(HASH) $(IMAGE_REF):latest
      
      





Así que ahora el contenedor ha sido creado y necesita ser probado para asegurarse de que cumpla con algunos de los requisitos mínimos. Para mi sitio web personal, es importante responder dos preguntas:



  1. "¿Se está iniciando el servidor web?"
  2. "¿Qué está volviendo?"


Esto se puede hacer ejecutando comandos de shell dentro del Makefile. Pero me resultó más fácil escribir un script en Python que inicia un contenedor usando Podman, envía una solicitud HTTP al contenedor, verifica si recibe una respuesta y luego lo limpia. El mecanismo de manejo de excepciones de Python (probar, excepto, finalmente) es ideal para esta tarea. Y es mucho más fácil implementar esta lógica en Python que en el shell:



#!/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())

      
      





La verificación puede resultar complicada. Por ejemplo, durante el proceso de compilación, puede solicitar que la respuesta contenga el hash de la revisión de Git. Y luego podemos verificar si la respuesta contiene el hash dado.



Si todo va bien con las pruebas, la imagen está lista para implementarse. Utilizo Google Cloud Run para alojar mi sitio web : le doy una imagen e instantáneamente me da una URL. Como muchos servicios similares, puede comunicarse con él a través de la interfaz de línea de comandos (CLI). Con Cloud Run, la implementación es tan simple como enviar imágenes (creadas localmente) a un registro de contenedor remoto e iniciar el proceso de implementación real con la herramienta de línea de comandos de gcloud .



Ahora creemos un objetivo de empuje.Yo uso Podman (pero puedes usar Skopeo o el mismo Docker en su lugar).



push:

  podman push --remove-signatures $(IMAGE_REF):$(HASH)

  podman push --remove-signatures $(IMAGE_REF):latest

      
      





Después de que se cargue la imagen, use el comando gcloud run deploy para implementar la última versión de la imagen en el proyecto y animarla.



Creemos el objetivo nuevamente en el Makefile. En este archivo, puedo crear variables para los argumentos --platform y --region para no tener que recordarlos cada vez. De lo contrario, tendría que inyectarlos fuera de mi cabeza cada vez que despliegue una nueva imagen. Y escribo tan pocas veces para mi blog personal que no hay posibilidad de que recuerde estas variables.



rollout:

  gcloud run deploy $(PROJECT) --image $(IMAGE_REF):$(HASH) --platform $(PLATFORM) --region $(REGION)

      
      





Metas adicionales



Lanzamiento local



Al probar CSS u otros cambios de código, quiero probar el trabajo del proyecto localmente, sin implementarlo en un servidor remoto. Para hacer esto, agregaré el destino run_local a mi Makefile . Selecciona el contenedor de acuerdo con mi compromiso actual y abre la URL de la página desde el servidor web local en el navegador.



En las implementaciones tradicionales, make no tiene una forma confiable de saber cuál es el objetivo. Después de todo, puede ser tanto el nombre de la acción como el nombre del archivo. Make simplemente busca en el disco un archivo llamado como destino. Si existe un archivo de este tipo, el destino se considera el nombre del archivo.



Por lo tanto, debe declarar explícitamente los objetivos como abstractos (es decir, acciones). Para hacer esto, simplemente agregue la palabra clave .PHONY.



Creemos un objetivo abstracto run_local:



.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)

      
      





También utilizo una variable para el nombre del navegador, por lo que puedo probar en diferentes navegadores si quiero. Cuando ejecuto make run_local , el sitio se abre en Firefox de forma predeterminada. Para probar lo mismo en Google Chrome, tengo que modificar la llamada make run_local agregando BROWSER = 'google-chrome' .



Limpieza de contenedores e imágenes viejos



Cuando se trabaja con contenedores, limpiar imágenes antiguas es una tarea frustrante, especialmente con iteraciones frecuentes. Así que agreguemos objetivos al Makefile para realizar estas tareas. Si el contenedor no existe, Podman o Docker regresarán del proceso de limpieza con el código 125. Desafortunadamente, make espera que cada comando devuelva 0 (si todo está bien) o salga (si algo está mal). Por lo tanto, debe escribir el siguiente procesamiento en bash :



#!/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 limpieza de imágenes requiere una lógica más compleja, pero puede hacerlo todo en el Makefile. Para facilitar esto, agregaré una etiqueta (a través del Containerfile) a la imagen (en el momento de su creación). Esto facilita la búsqueda de todas las imágenes con las etiquetas dadas. El más reciente de estos se puede identificar por la etiqueta : latest . Y ahora se pueden eliminar todas las imágenes excepto las más recientes (etiquetadas: últimas):



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

      
      





Atado por un objetivo



Por el momento, mi Makefile incluye comandos para crear y etiquetar imágenes, probar, cargar imágenes, implementar nuevas versiones, limpiar imágenes y ejecutar una versión local. Ejecutar cada uno de ellos con make image_build && make image_tag && make test y así sucesivamente es mucho más fácil que ejecutar cada uno de los comandos dentro de estas llamadas. Pero todo se puede simplificar aún más.



Quiero hacer en mi proyecto predeterminado para hacer todo, desde la creación de imágenes hasta las pruebas, la implementación y la limpieza:



.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
      
      





Un Makefile puede ejecutar varios objetivos a la vez. Agrupemos los objetivos image_build y image_tag en un solo objetivo de compilación . Ahora, para ejecutarlos, solo necesita llamar a make build . Además, también agruparé los objetivos de compilación, prueba, implementación y limpieza en todos los objetivos , lo que me permitirá ejecutarlos todos (en la secuencia especificada) en una llamada:



$ make all
      
      





o incluso más simple:



$ make
      
      





No solo contenedores



Con la ayuda de Makefile, puede combinar todos los comandos necesarios para construir, probar e implementar un proyecto. Esto puede simplificar y automatizar una gran cantidad de tareas.



Pero el Makefile también se puede utilizar para tareas de desarrollo: ejecutar pruebas unitarias, compilar binarios y generar sumas de comprobación.



Es una pena que Makefile no pueda escribir el código por nosotros ( guiños ).






Compre alojamiento VDS con unidades NVM rápidas y pago diario de Macleod hosting.






All Articles