Cuanto más avanza nuestro pequeño proyecto, más código producimos. Y como la mayoría de nuestra comunidad hoy en día, alojamos nuestro código en GitHub.
Así que durante el desarrollo discutimos las implicaciones de una arquitectura DownToZero para algo como un proceso de GitHub. Rápidamente identificamos dos tipos de acciones que necesitan realizarse en la infraestructura.
La primera es la compilación CI, que siempre debe proporcionar retroalimentación inmediata al desarrollador. Verifica el cumplimiento así como la integridad del código y usualmente está profundamente involucrada en el proceso de desarrollo. Estos trabajos son sensibles al tiempo porque alguien usualmente está esperando por ellos.
La segunda categoría que identificamos es un poco diferente. Con el auge de dependabot y otros escáneres de seguridad, vimos más y más pipelines que se activaban por estos bots. Lo importante aquí es que queremos ejecutar el pipeline para revisar nuestras dependencias y mantener nuestra base de código actualizada, pero al mismo tiempo, nadie está esperando esos pipelines. Por lo que no haría ninguna diferencia si esos pipelines se retrasaran.
Entonces veamos la segunda categoría y si podemos construir algo dentro de GitHub. Bueno, GitHub permite a cualquiera adjuntar runners autohospedados a cualquier proyecto (hostear tus propios runners). Si miras el proceso, es relativamente sencillo: descarga el runner, adjúntalo a tu organización o repositorio y luego ejecuta el script shell. También hay un pequeño ayudante que convierte este runner en un servicio systemd, para que no tengamos que iniciar y detener el servicio nosotros mismos.
Respecto a la distribución de trabajos, GitHub dice que el trabajo en cola se mantiene durante 24 horas. Dentro de ese periodo el trabajo debe ser tomado o expirará. Así que 24 horas es técnicamente tiempo suficiente para esperar a que salga el sol, sin importar cuándo fue generado el trabajo.
Con esto cubierto, comenzamos a mirar nuestra configuración local y cómo podemos lograr dicha planificación de capacidad. Nuestra configuración actual es así.
No tenemos ninguna batería de almacenamiento conectada, porque eso haría que todo el sistema sea más caro y complejo.
Todas las métricas, como la producción de energía del panel solar o el consumo energético de los servidores, son monitoreadas por dispositivos independientes tasmota (CloudFree EU Smart Plug).
Así que conectamos todo. Para conveniencia, instalamos Ubuntu 22.10 (el mismo usado para el runner alojado en GitHub) en nuestras máquinas. También instalamos la cadena de herramientas necesaria, como rustup, gcc-musl, protobuf.
Ahora, escribimos 3 servicios independientes para systemd.
El primer servicio siempre está en ejecución y lee la producción energética de HomeAssistant (aquí se agregan nuestros datos de energía). También toma en cuenta qué otros dispositivos están corriendo y cuánta energía están consumiendo actualmente. Luego implementa el siguiente modelo de estados:
Definición del Servicio systemd
[Unit]
Description=dtz edge Service
[Service]
Type=simple
WorkingDirectory=/root/dtz-edge
ExecStart=!/root/dtz-edge/busy.sh
Restart=always
[Install]
Alias=dtz-edge
WantedBy=multi-user.target
Script shell busy.sh (versión abreviada)
#!/bin/bash
for (( ; ; ))
do
POWER=`curl -H 'Authorization: Bearer token1' -H "Content-Type: application/json" http://192.168.178.76:8123/api/states/sensor.solar_panel_energy_power 2> /dev/null | jq -r .state`
METER=`curl -H 'Authorization: Bearer token1' -H "Content-Type: application/json" http://192.168.178.76:8123/api/states/sensor.tasmota_energy_power_4 2> /dev/null | jq -r .state`
SALDO=$((POWER - METER))
echo "Saldo: $SALDO (solar: $POWER)"
CURRENT_HOUR=`date +%H`
if [ $CURRENT_HOUR -gt 17 ]; then
service cheap-energy stop
service actions.runner.DownToZero-Cloud.dtz-edge1 stop
echo "sleep till tomorrow (10h)"
rtcwake -m disk -s 36000
fi
if [ $SALDO -gt 70 ]; then
echo "más de 70: $SALDO"
service cheap-energy start
service actions.runner.DownToZero-Cloud.dtz-edge1 start
sleep 300;
else
service cheap-energy stop
rtcwake -m mem -s 660
fi
done
Este servicio solo mantiene el estado de que energía barata está disponible. Así que cuando este servicio systemd está corriendo, significa que hay energía disponible, y cuando está detenido, todos los workers deberían apagarse.
Por eso usamos este servicio como un proxy para facilitar la gestión.
[Unit]
Description=cheap energy
[Service]
Type=simple
WorkingDirectory=/root/dtz-edge
ExecStart=!/root/dtz-edge/cheap-energy.sh
Restart=always
[Install]
Alias=cheap-energy
WantedBy=multi-user.target
El script que ejecutamos aquí es simplemente un comando de sleep.
#!/bin/bash
sleep infinity
Seguimos las instrucciones proporcionadas por GitHub e instalamos el runner como un servicio systemd.
sudo ./svc.sh install
Esto ya nos dio la definición correcta del servicio y lo único que necesitábamos cambiar fue la línea de dependencia del servicio.
Porque ahora queremos que este servicio se ejecute solo cuando el servicio cheap-energy esté funcionando, y que también se detenga cuando el servicio cheap-energy se detenga.
Así que modificamos la definición de nuestro servicio (actions.runner.DownToZero-Cloud.dtz-edge1.service
) para incluir la descripción BindsTo
.
[Unit]
Description=GitHub Actions Runner (DownToZero-Cloud.dtz-edge1)
After=network.target
BindsTo=cheap-energy.service
[Service]
ExecStart=/home/user1/gh-dtz-org/runsvc.sh
User=user1
WorkingDirectory=/home/user1/gh-dtz-org
KillMode=process
KillSignal=SIGTERM
TimeoutStopSec=5min
[Install]
WantedBy=multi-user.target
Ahora que tenemos configurada la parte hardware de la solución, volvamos al lado de GitHub.
Ahora tenemos 2 tipos de runners en nuestra interfaz de GitHub. Uno es el runner alojado en GitHub, en el que queremos que se ejecuten nuestras tareas tipo 1, y otro es nuestro pool dtz-edge
que solo se activa cuando hay suficiente energía solar.
Vamos a dividir nuestras definiciones de pipeline.
Para los trabajos tipo 1, todo puede permanecer como un pipeline normal de GitHub.
name: build
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
Para los trabajos tipo 2, es decir, trabajos que queremos ejecutar de forma retrasada en las máquinas alimentadas por energía solar, solo necesitamos definir la sección on
para incluir los escenarios que queremos soportar aquí. En nuestro caso comenzamos haciendo esto para todos los pull-requests. Luego, lo único que se debe cambiar es la sentencia runs-on
. Aquí colocamos nuestro runner recién creado.
name: pr
on:
workflow_dispatch:
pull_request:
jobs:
test:
name: coverage
runs-on: self-hosted
Así que ahora cada vez que dependabot nos envíe algunas actualizaciones para fusionar, o algún otro bot quiera verificar tests y cobertura de código, esos trabajos se ejecutarán cuando tengamos los recursos para hacerlo.
Como bonus adicional, ya no tenemos que pagar por estos runners extra. Los runners on-premises son gratuitos (en el sentido de la política de precios de GitHub).