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 deben 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 los está esperando.
La segunda categoría que identificamos es un poco diferente. Con el auge de dependabot y otros escáneres de seguridad, vimos que cada vez más pipelines se activaban por estos bots. Lo que pasa es que queremos ejecutar la pipeline para revisar nuestras dependencias y mantener nuestra base de código actualizada, pero al mismo tiempo, nadie está esperando esas pipelines. Así que no haría diferencia que esas pipelines se retrasaran.
Por eso revisemos la segunda categoría y veamos si podemos construir algo dentro de GitHub. Bueno, GitHub permite a cualquiera adjuntar runners autoalojados a cualquier proyecto (alojar tus propios runners). Si miras el proceso, es relativamente sencillo: descargar el runner, adjuntarlo a tu organización o repositorio y luego ejecutar el script shell. También hay un pequeño auxiliar que convierte este runner en un servicio systemd, para que no tengamos que iniciar y detener el servicio nosotros mismos.
Mirando 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 se haya generado el trabajo.
Con esto cubierto, empezamos a mirar nuestra configuración local y cómo podemos lograr tal planificación de capacidad. Nuestra configuración actual es como sigue.

No tenemos ningún almacenamiento con baterías conectado, porque eso haría todo el sistema más caro y complejo.
Todas las métricas, como la producción energética 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. Por comodidad, instalamos ubuntu 22.10 (el mismo usado para el runner alojado en GitHub) en nuestras máquinas. También instalamos la cadena de herramientas que necesitábamos, como rustup, gcc-musl, protobuf.
Ahora, escribimos 3 servicios systemd independientes.
El primer servicio está siempre corriendo y lee la producción energética desde HomeAssistant (aquí es donde se agregan nuestros datos de energía). También toma en cuenta qué otros dispositivos están corriendo y cuánto consumo de energía tienen. 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 "more then 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 hay energía barata disponible. Así que cuando este servicio systemd está activo, significa que hay energía disponible; cuando se detiene, 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 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 proporcionó la definición correcta del servicio y la única cosa que necesitábamos cambiar era la línea de dependencia del servicio. Porque ahora queremos que este servicio corra siempre que el servicio cheap-energy esté activo, y también se detenga cuando se detenga cheap-energy.
Así que cambiamos 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 la parte de hardware de la solución configurada, volvamos al lado de GitHub.
Ahora tenemos 2 tipos de runners en nuestra interfaz de GitHub. Uno es el runner alojado por GitHub, donde queremos que se ejecuten nuestras tareas tipo 1, y otro es nuestro grupo dtz-edge que solo se activa cuando hay suficiente energía solar.

Dividamos nuestras definiciones de pipeline.
Para los trabajos tipo 1, todo puede mantenerse como una 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 con retardo en las máquinas alimentadas por energía solar, solo tenemos que definir la sección de disparadores on para incluir los escenarios que deberían ser soportados aquí. En nuestro caso empezamos haciendo esto para todos los pull-requests. Luego, lo único que se necesita cambiar es la expresión runs-on. Aquí colocamos nuestro runner recién generado.
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 revisar tests y cobertura de código, esos trabajos se ejecutarán cuando tengamos los recursos para hacerlo.
Como un beneficio adicional, tampoco tenemos que pagar más por estos runners adicionales. Los runners on-premises son gratuitos (en el sentido de facturación de GitHub).