Più il nostro piccolo progetto avanza, più codice produciamo. E come la maggior parte della nostra community al giorno d’oggi, ospitiamo il nostro codice su GitHub.
Quindi durante lo sviluppo abbiamo discusso le implicazioni di un’architettura DownToZero per qualcosa come un processo GitHub. Abbiamo rapidamente identificato due tipi di azioni che devono essere eseguite sull’infrastruttura.
La prima è la build CI, che dovrebbe fornire sempre un riscontro immediato allo sviluppatore. Controlla la conformità così come l’integrità del codice ed è solitamente profondamente coinvolta nel processo di sviluppo. Questi job sono sensibili al tempo perché di solito qualcuno li sta aspettando.
La seconda categoria che abbiamo identificato è un po’ diversa. Con l’aumento di dependabot e altri scanner di sicurezza, abbiamo visto sempre più pipeline attivate da questi bot. La cosa è che vogliamo eseguire la pipeline per controllare le dipendenze e mantenere il nostro codice aggiornato, ma allo stesso tempo nessuno sta aspettando quelle pipeline. Quindi non farebbe alcuna differenza se quelle pipeline fossero rimandate.
Quindi diamo un’occhiata alla seconda categoria e vediamo se possiamo costruire qualcosa all’interno di GitHub. Beh, GitHub permette a chiunque di collegare runner self-hosted a qualsiasi progetto (hosting your own runners). Se guardi il processo, è relativamente semplice: scaricare il runner, collegarlo alla tua organizzazione o repo e poi eseguire lo script shell. C’è anche un piccolo helper che trasforma questo runner in un servizio systemd, così non dobbiamo avviare e fermare il servizio manualmente.
Guardando la distribuzione dei job, GitHub dice che il job in coda viene mantenuto per 24 ore. Entro quel lasso di tempo il job deve essere preso in carico o andrà in timeout. Quindi 24 ore sono tecnicamente abbastanza per aspettare che il sole sorga, indipendentemente da quando il job è stato creato.
Con questo chiarito, abbiamo iniziato a guardare la nostra configurazione locale e come possiamo raggiungere tale pianificazione della capacità. La nostra configurazione attuale è così.

Non abbiamo alcun accumulo a batteria collegato, perché questo renderebbe l’intero sistema più costoso e complesso.
Tutte le metriche, come la produzione di energia del pannello solare o il consumo energetico dei server, sono tracciate da dispositivi indipendenti tasmota (CloudFree EU Smart Plug).
Quindi abbiamo collegato tutto. Per comodità, abbiamo installato Ubuntu 22.10 (lo stesso usato per il runner ospitato da GitHub) sulle nostre macchine. Abbiamo anche installato la toolchain di cui avevamo bisogno, come rustup, gcc-musl, protobuf.
Ora, abbiamo scritto 3 servizi systemd indipendenti.
Il primo servizio è sempre attivo e legge l’output energetico da HomeAssistant (qui è dove vengono aggregate le nostre informazioni energetiche). Tiene anche in considerazione quali altri dispositivi sono attualmente in funzione e quanta energia stanno già consumando. Implementa quindi il seguente modello di stato:

Definizione del servizio 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 busy.sh (versione accorciata)
#!/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
Questo servizio tiene solo lo stato che l’energia economica è disponibile. Quindi quando questo servizio systemd è in esecuzione, significa che c’è energia disponibile; quando è fermo, tutti i worker dovrebbero spegnersi. Quindi usiamo questo servizio come proxy per rendere più semplice la gestione.
[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
Lo script che eseguiamo qui è solo un comando sleep.
#!/bin/bash
sleep infinity
Abbiamo seguito le istruzioni fornite da GitHub e installato il runner come servizio systemd.
sudo ./svc.sh install
Questo ci ha già fornito la definizione del servizio corretta e l’unica cosa che abbiamo dovuto cambiare è stata la riga di dipendenza del servizio. Perché ora vogliamo che questo servizio venga eseguito ogni volta che il servizio cheap-energy è attivo, e che si fermi quando cheap-energy viene fermato.
Quindi abbiamo modificato la definizione del nostro servizio (actions.runner.DownToZero-Cloud.dtz-edge1.service) per includere la descrizione 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
Ora che abbiamo configurato la parte hardware della soluzione, torniamo alla parte GitHub.
Ora abbiamo 2 tipi di runner nella nostra UI di GitHub. Uno è il runner ospitato da GitHub, sul quale vogliamo eseguire il nostro task di tipo 1, e uno è il nostro pool dtz-edge che si attiva solo quando c’è abbastanza energia solare.

Dividiamo le nostre definizioni di pipeline.
Per i job di tipo 1, tutto può rimanere come una normale pipeline GitHub.
name: build
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
Per i job di tipo 2, cioè i job che vogliamo eseguire in ritardo sulle macchine alimentate a energia solare, dobbiamo solo definire la sezione on-trigger per includere gli scenari che vogliamo supportare qui. Nel nostro caso abbiamo iniziato facendo questo per tutte le pull request. Poi l’unica cosa che deve essere cambiata è l’istruzione runs-on. Qui abbiamo inserito il nostro runner appena creato.
name: pr
on:
workflow_dispatch:
pull_request:
jobs:
test:
name: coverage
runs-on: self-hosted
Quindi ora ogni volta che dependabot ci invia degli aggiornamenti da unire, o qualche altro bot vuole controllare i test e la copertura del codice, quei job verranno eseguiti quando avremo le risorse per farlo.
Come vantaggio aggiuntivo, non dobbiamo più pagare per questi runner extra. I runner on-prem sono gratuiti (nel senso della tariffazione di GitHub).