Un Runner GitHub Alimentato a Energia Solare

created: sabato, apr 15, 2023

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ì.

solar-powered-runner

  1. abbiamo pannelli solari che producono energia
  2. abbiamo macchine locali che possono consumare quell’energia e hanno accesso a Internet
  3. GitHub fornisce la coda di lavoro persistente per il nostro runner

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.

1) Servizio Dtz-Edge

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:

solar state model

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

2) Servizio Cheap-Energy

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

3) Servizio GitHub Runner

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.

solar pool in GitHub

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).