Mi imagen de CertBoot
06 Dec 2024
Categorías: DockerfilesEn este post se describe como se ha creado la imagen de CertBoot para que vaya comprobando y autorenovando los certificados cada cierto tiempo.
Introducción
Hace un tiempo que tenia scripts para lanzar a CertBoot en el host, pero pensé, ¿porque no lo pongo en Docker desde la imagen oficial?.
Revisando la imagen oficial, vi que era muy fácil lanzarlo una vez para renovar los certificados, pero a mi me gustaría algo más automático.
También vi imágenes personalizadas que tiraban de cron, etc.. Pero para eso, mi imagen, y a poder ser sencilla. Es por ello que me lié la manta a la cabeza y preparé una imagen de CertBoot lo más sencilla posible que partiera de la oficial para mantener todas las características.
El código fuente es público y la explicación de como utilizar esta imagen personalizada se puede encontrar en Github
La solución
La solución ha sido lo más sencilla posible, añadir el comando sleep, y un entrypoint lo más sencillo posible.
Como se ha implementado la nueva imagen
Se ha dejado la imagen lo más similar posible a la imagen base de CertBoot de esta manera la nueva imagen funciona como si fuera la original pero con el añadido que se ha comentado.
A continuación se expondrán los ficheros utilizados para su creación y el porque de las cosas.
El Dockerfile
Para empezar, se incluye el Dockerfile para generar la imagen de Docker.
# Utilizamos la imagen oficial de certbot como base
FROM certbot/certbot:latest
# CertBoot latest se basa en Alpine, se actualiza el sistema
RUN apk update && \
apk add --no-cache bash && \
apk upgrade && \
rm -rf /var/cache/apk/*
# Copiar el script de inicio al contenedor
COPY script_inicio.sh /script_inicio.sh
# Definir el script como entrypoint
ENTRYPOINT ["/bin/sh", "/script_inicio.sh"]
Solo se actualiza el sistema (ya que se construye, que sea lo más actual posible), se añade bash y el entrypoint. Poca cosa.
El script-inicio.sh
El propio script que se utiliza está bastante comentado y se puede seguir bien, es por ello, que se añadirá a continuación y se harán los comentarios clave al final.
#!/bin/bash
# Para una salida limpia si Certboot está en marcha
salir_bien() {
echo "Apagando CERTBOOT"
if kill -0 "${CERTBOOT_PID}" 2>/dev/null; then
kill -SIGTERM "${CERTBOOT_PID}"
wait "${CERTBOOT_PID}" || echo "CERTBOOT ya estaba detenido"
fi
}
# Si se reciben las señales
# Si se recibe SIGTERM, se mata de manera ordena el proceso (lo envía docker por defecto para apagar contenedores)
# INT es SIGINT que sería el Ctrl+c
trap "salir_bien" SIGTERM INT
# Si hay un SIGQUIT se hace un exit. SIGQUIT tiene volcado de memoria
trap "exit" QUIT
while true; do
certbot $@
# Si existe la variable UID, se cambia el propietario de los certificados
if [ -n "$UID" ]; then
echo "Cambiando propietario de los ficheros a uid--> ${UID}"
chown -R $UID /etc/letsencrypt
fi
# Si existe la variable GID, se cambia el grupo de los certificados
if [ -n "$GID" ]; then
echo "Cambiando grupo propietario de los fichero al gid --> ${GID}"
chgrp -R $GID /etc/letsencrypt
fi
# Permiso total al propietario y de lectura al grupo
echo "Cambiando permisos de fichero a 740"
chmod -R 740 /etc/letsencrypt
# Por si alguien utilizaba la imagen anterior, se mantiene que funcione con SLEEP
if [ -n "$SLEEP" ]; then
DORMIR=$SLEEP
fi
# Si existe la variable DORMIR, se espera es tiempo, y sino se sale
if [ -n "$DORMIR" ]; then
# Tiempo a dormir
echo "Durmiendo durante ${DORMIR}"
sleep $DORMIR
else
break
fi
done &
# Guardar el PID del proceso
CERTBOOT_PID=$!
# Se espera a que termine el proceso y recoge el código de salida
wait -n "${CERTBOOT_PID}"
EXIT_CODE=$?
# Se sale
echo "Saliendo con código de salida ${EXIT_CODE}"
exit "${EXIT_CODE}"
El resumen es un bucle infinito que ejecuta el CMD
con el que se lanza el contenedor, cambia el propietario y el grupo de los ficheros (según una variable de entorno) y se duerme un tiempo definido en otra variable de entorno y se vuelve a repetir.
Personalmente, me pareció la manera más sencilla de tenerlo funcionando y renovando los certificados cuando toca.
Como se actualiza la imagen
También me gustaría comentar que esta imagen no se crea una vez, se sube a DockerHub y no se hace nada más en el tiempo.
En el servidor está trabajando Gitea para mi uso personal, y también tiene configurado el trabajar con runners, por tanto, puedo ejecutar acciones de Gitea para mantener la imagen actualizada, a continuación se explica que se hace a través del propio código fuente de la acción (ATENCIÓN, se ejecuta en contenedor personalizado!!!)
name: Crear nueva imagen al actualizar
run-name: Se crea nueva imagen y se sube a dockerhub
on:
push:
branches:
- main
# No se lanza la acción si la actualización es del readme.md
paths:
- '**/*' # Todos los archivos del repositorio
- '!README.md'
schedule:
# - cron: "*/5 * * * *" # Ejecuta cada 5 min para pruebas
- cron: "1 2 * * *" # Ejecuta todos los días a las 02:1h
jobs:
"Actualización de imagen":
runs-on: custom
env:
IMAGEN_BASE: 'docker.io/certbot/certbot:latest'
IMAGEN_DEST: 'docker.io/pepramon/certbot:latest'
steps:
- name: Comprobar si es un push
run: |
if [ "${% raw %}{{ github.event_name }}{% endraw %}" == "push" ]; then
echo "Es un push real. Se debe construir la imagen."
echo "construir=true" >> $GITHUB_ENV
else
echo "No es un push. Continuando con las verificaciones cambio imagen o actualizaciones."
fi
- name: Comprobar si hay actualizaciones de Alpine
if: env.construir != 'true'
run: |
echo "Se hacen comprobaciones de actualizaciones de Alpine. Bajando imagen"
podman pull $IMAGEN_DEST
comando="apk update > /dev/null 2>&1 && apk list -u | wc -l"
paquetes_actualizar=$(podman run --entrypoint "" $IMAGEN_DEST bash -c "$comando")
echo "Hay $paquetes_actualizar paquetes por actualizar de Alpine"
if [ "$paquetes_actualizar" -gt 0 ]; then
echo "Actualizaciones disponibles en Alpine. Hay que construir imagen"
echo "construir=true" >> $GITHUB_ENV
else
echo "No hay actualizaciones disponibles en Alpine."
fi
- name: Comprobando si imagen base ha cambiado
if: env.construir != 'true'
run: |
echo "Realizando pull de las imagenes para comprobar si hay cambios"
podman pull $IMAGEN_BASE
# Se guarda que se ha hecho pull
echo "pullEcho=true" >> $GITHUB_ENV
# Se obtiene el sha de la imagen base
base=$(podman inspect $IMAGEN_BASE --format '{% raw %}{{index .RepoDigests 0}}{% endraw %}' | awk -F'@' '{print $2}')
# Se obtiene el guardado en la imagen construida
imagen=$(podman inspect $IMAGEN_DEST --format '{% raw %}{{ index .Config.Labels "base_digest" }}{% endraw %}')
# Comprobación de si ha cambiado o no
if [ "$base" == "$imagen" ]; then
echo "La imagen base no ha cambiado."
else
echo "La imagen base ha cambiado. Hay que reconstruir la imagen derivada."
echo "construir=true" >> $GITHUB_ENV
fi
- name: Checkout repositorio
if: env.construir == 'true'
uses: http://gitea:3000/acciones/checkout@v1
- name: Hacer pull imagen base
if: {% raw %}${{ env.pullEcho != 'true' && env.construir == 'true' }}{% endraw %}
run: |
echo "Haciendo pull de $IMAGEN_BASE"
podman pull $IMAGEN_BASE
- name: Se construye la imagen
if: env.construir == 'true'
run: |
# Obtén el digest de la imagen base
base_digest=$(podman inspect $IMAGEN_BASE --format '{% raw %}{{index .RepoDigests 0}}{% endraw %}' | awk -F'@' '{print $2}')
# Construcción de la imagen
podman build -t $IMAGEN_DEST --label base_digest="$base_digest" .
- name: Logeo en dockerhub
if: env.construir == 'true'
run: podman login -u pepramon -p {% raw %}${{ secrets.DOCKERHUB }}{% endraw %} docker.io
- name: Se sube la imagen
if: env.construir == 'true'
run: podman push $IMAGEN_DEST
Aunque se puede seguir bastante bien, hay que comentar que mi runner personalizado tiene instalado Podman en lugar de Docker, ¿porque? pues para probar y aprender.
Hay que tener cuidado, ya que se utilizan nombres de host internos de mi servidor, por tanto, no funcionará fuera de mi instalación y configuración, pero básicamente hace:
- La acción se ejecuta todos los días a las 2:1
- Si es un push, al repo, si o si se hace la construcción de la imagen
- Si no es un push, se construirá de nuevo la imagen si:
- Hay actualizaciones de Alpine en la imagen ya construida
- La imagen base ha cambiado
- El resto de pasos, son para construir la imagen y subirla a DockerHub.
Hay que añadir un comentario a la construcción, como no sabia muy bien como saber si la imagen base con la que se había generado la imagen personalizada había cambiado, se ha añadido ese dato a la propia imagen generada.
Para ello, se obtiene la suma SHA de la imagen base mediante base_digest=$(podman inspect $IMAGEN_BASE --format '{% raw %}{{index .RepoDigests 0}}{% endraw %}' | awk -F'@' '{print $2}')
, y al construir la imagen se le añade una etiqueta llamada base_digest
donde se almacena desde que imagen base se ha construido (comando de construcción podman build -t $IMAGEN_DEST --label base_digest="$base_digest" .
)
Con esto se puede estar seguro de detectar de manera fácil si la imagen base ha cambiado o no (ver paso llamado Comprobando si imagen base ha cambiado
)
Conclusiones
Con esto, queda explicada la creación de mi imagen de CertBoot que utilizo conjuntamente con la de Nginx para servir este blog.