Più il nostro piccolo progetto avanza, più codice produciamo. E come la maggior parte della nostra comunità 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 sempre fornire un feedback immediato allo sviluppatore. Controlla la conformità così come l’integrità del codice ed è solitamente coinvolta profondamente nel processo di sviluppo. Questi lavori sono sensibili al tempo perché qualcuno di solito li sta aspettando.
La seconda categoria che abbiamo identificato è un po’ diversa. Con la crescita di dependabot e altri scanner di sicurezza, abbiamo visto sempre più pipeline attivate da questi bot. La questione è che vogliamo eseguire la pipeline per controllare le dipendenze e mantenere aggiornato il nostro codice, ma allo stesso tempo, nessuno aspetta quelle pipeline. Quindi non farebbe alcuna differenza se quelle pipeline fossero ritardate.
Quindi guardiamo 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 si guarda il processo, è abbastanza semplice, scaricare il runner, collegarlo alla propria organizzazione o repo e poi avviare 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 alla distribuzione dei lavori, GitHub dice che il lavoro in coda viene mantenuto per 24 ore. In questo arco di tempo il lavoro deve essere preso in carico o scadrà. Quindi tecnicamente 24 ore sono abbastanza tempo per aspettare che sorga il sole, indipendentemente da quando il lavoro è stato creato.
Coperto questo aspetto, abbiamo iniziato a guardare alla nostra configurazione locale e come possiamo ottenere questa pianificazione della capacità. La nostra configurazione attuale è così.

Non abbiamo alcuna batteria collegata, perché renderebbe l’intero sistema più costoso e complesso.
Tutte le metriche, come la potenza prodotta dal 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 necessaria, come rustup, gcc-musl, protobuf.
Ora, abbiamo scritto 3 servizi systemd indipendenti.
Il primo servizio è sempre in esecuzione e legge la potenza prodotta da HomeAssistant (qui vengono aggregati i nostri dati energetici). Tiene anche conto di quali altri dispositivi sono attualmente attivi e quanta energia stanno già consumando. Quindi implementa 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 shell (versione ridotta)
#!/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 mantiene solo lo stato che indica che energia economica è disponibile. Quindi, quando questo servizio systemd è in esecuzione, significa che è disponibile energia; quando è fermato, tutti i worker dovrebbero spegnersi.
Usiamo quindi questo servizio come proxy per facilitare 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 è semplicemente 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à dato la definizione corretta del servizio e l’unica cosa che abbiamo dovuto cambiare è stata la linea di dipendenza del servizio. Perché ora vogliamo che questo servizio venga eseguito ogni volta che il servizio cheap-energy è attivo, e che venga fermato quando cheap-energy viene fermato.
Quindi abbiamo modificato la definizione del 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 interfaccia GitHub. Uno è il runner ospitato da GitHub, su cui vogliamo far girare i lavori di tipo 1, e uno è il nostro pool dtz-edge che si avvia solo quando c’è abbastanza energia solare.

Divideremo le definizioni delle pipeline.
Per i lavori di tipo 1, tutto può rimanere come una pipeline GitHub normale.
name: build
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
Per i lavori di tipo 2, ovvero i lavori che vogliamo eseguire in modo ritardato sulle macchine alimentate a energia solare, dobbiamo solo definire la sezione on trigger per includere gli scenari che vogliamo supportare. Nel nostro caso abbiamo iniziato facendo così per tutte le pull-request. L’unica cosa che serve modificare è la linea runs-on. Qui abbiamo inserito il nostro runner auto-ospitato.
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 test e copertura del codice, quei lavori verranno eseguiti ogni volta che avremo le risorse per farlo.
Come bonus aggiuntivo, non dobbiamo più pagare questi runner extra. I runner on-premise sono gratuiti (nel senso della tariffazione GitHub).