Je weiter unser kleines Projekt voranschreitet, desto mehr Code produzieren wir. Und wie die meisten in unserer Community hosten wir unseren Code heutzutage auf GitHub.
Während der Entwicklung haben wir also die Implikationen einer DownToZero-Architektur für etwas wie einen GitHub-Prozess diskutiert. Wir haben schnell zwei Arten von Aktionen identifiziert, die auf der Infrastruktur ausgeführt werden müssen.
Die erste ist der CI-Build, der dem Entwickler immer sofortiges Feedback geben soll. Er prüft die Einhaltung sowie die Code-Integrität und ist normalerweise eng in den Entwicklungsprozess eingebunden. Diese Jobs sind zeitkritisch, da meistens jemand auf sie wartet.
Die zweite Kategorie, die wir identifiziert haben, ist etwas anders. Mit dem Aufstieg von Dependabot und anderen Sicherheitsscannern sahen wir immer mehr Pipelines, die von diesen Bots ausgelöst werden. Das Besondere daran ist: Wir wollen die Pipeline ausführen, um unsere Abhängigkeiten zu überprüfen und unseren Code aktuell zu halten, aber gleichzeitig wartet niemand auf diese Pipelines. Es würde also keinen Unterschied machen, wenn diese Pipelines verzögert würden.
Schauen wir uns also die zweite Kategorie an und prüfen, ob wir etwas innerhalb von GitHub bauen können. GitHub erlaubt es jedem, selbstgehostete Runner an jedes Projekt anzuhängen (eigene Runner hosten). Wenn man sich den Prozess ansieht, ist er relativ einfach: Den Runner herunterladen, an die Organisation oder das Repository anhängen und dann das Shell-Skript ausführen. Es gibt auch einen kleinen Helfer, der diesen Runner in einen systemd-Dienst verwandelt, so dass wir den Dienst nicht selbst starten und stoppen müssen.
Bezüglich der Job-Verteilung sagt GitHub, dass der wartende Job 24 Stunden gehalten wird. Innerhalb dieses Zeitfensters muss der Job übernommen werden, sonst läuft er aus. 24 Stunden sind also technisch genug Zeit, um auf den Sonnenaufgang zu warten, unabhängig davon, wann der Job erzeugt wurde.
Nachdem das geklärt war, haben wir uns unser lokales Setup angesehen und wie wir eine solche Kapazitätsplanung erreichen können. Unser aktuelles Setup sieht so aus.
Wir haben keinen Batteriespeicher angeschlossen, weil das das gesamte System teurer und komplexer machen würde.
Alle Metriken, wie die Energieausbeute des Solarpanels oder der Energieverbrauch der Server, werden von unabhängigen tasmota-Geräten (CloudFree EU Smart Plug) erfasst.
Also haben wir alles angeschlossen. Für die Bequemlichkeit haben wir Ubuntu 22.10 (das gleiche wie für den gehosteten GitHub-Runner) auf unseren Maschinen installiert. Wir haben auch die benötigte Toolchain installiert, wie rustup, gcc-musl, protobuf.
Jetzt haben wir 3 unabhängige systemd-Dienste geschrieben.
Der erste Dienst läuft immer und liest die Energieausbeute von HomeAssistant (hier werden unsere Energiedaten aggregiert). Er berücksichtigt auch, welche anderen Geräte gerade laufen und wie viel Energie diese bereits verbrauchen. Dann implementiert er folgendes Zustandsmodell:
Systemd-Dienstdefinition
[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
Busy.sh Shell-Skript (verkürzte Version)
#!/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 "mehr als 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
Dieser Dienst hält nur den Zustand, dass günstige Energie verfügbar ist. Wenn dieser systemd-Dienst läuft, bedeutet das, es ist Energie verfügbar; wenn er gestoppt wird, sollen alle Worker herunterfahren.
Wir verwenden diesen Dienst also als Proxy, um das Management zu erleichtern.
[Unit]
Description=günstige Energie
[Service]
Type=simple
WorkingDirectory=/root/dtz-edge
ExecStart=!/root/dtz-edge/cheap-energy.sh
Restart=always
[Install]
Alias=cheap-energy
WantedBy=multi-user.target
Das Skript, das wir hier ausführen, ist nur ein sleep-Befehl.
#!/bin/bash
sleep infinity
Wir folgten den Anweisungen von GitHub und installierten den Runner als systemd-Dienst.
sudo ./svc.sh install
Das gab uns bereits die korrekte Dienstdefinition und das Einzige, was wir anpassen mussten, war die Abhängigkeitszeile des Dienstes. Denn nun wollen wir, dass dieser Dienst läuft, sobald der cheap-energy-Dienst läuft, und dass dieser Dienst stoppt, wenn cheap-energy gestoppt wird.
Also änderten wir unsere Dienstdefinition (actions.runner.DownToZero-Cloud.dtz-edge1.service
), um die BindsTo
-Beschreibung einzufügen.
[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
Nachdem der Hardware-Teil der Lösung steht, wenden wir uns wieder GitHub zu.
Wir haben nun 2 Arten von Runnern in unserer GitHub-Oberfläche. Einen GitHub-gehosteten Runner, auf dem wir unsere Typ-1-Tasks laufen lassen wollen, und unseren dtz-edge
-Pool, der nur startet, wenn genügend Solarstrom verfügbar ist.
Lassen Sie uns unsere Pipeline-Definitionen aufteilen.
Für die Typ-1-Jobs kann alles wie bei einer normalen GitHub-Pipeline bleiben.
name: build
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
Für die Typ-2-Jobs, also Jobs, die verzögert auf den solarbetriebenen Maschinen laufen sollen, muss im on
-Trigger-Abschnitt nur definiert werden, welche Szenarien hier unterstützt werden sollen. In unserem Fall haben wir damit begonnen, dies für alle Pull Requests zu tun. Dann muss nur noch die runs-on
-Anweisung geändert werden. Hier setzen wir unseren neu erzeugten Runner ein.
name: pr
on:
workflow_dispatch:
pull_request:
jobs:
test:
name: coverage
runs-on: self-hosted
Jetzt laufen alle Jobs, die Dependabot uns zum Mergen sendet, oder andere Bots, die Tests und Code Coverage prüfen wollen, immer dann, wenn wir die Ressourcen dafür haben.
Als zusätzlicher Bonus müssen wir für diese zusätzlichen Runner auch nicht mehr zahlen. On-Premise-Runner sind (im Sinne der GitHub-Preise) kostenlos.