Plus notre petit projet progresse, plus nous produisons de code. Et comme la plupart de notre communauté de nos jours, nous hébergeons notre code sur GitHub.
Ainsi, pendant le développement, nous avons discuté des implications d’une architecture DownToZero pour quelque chose comme un processus GitHub. Nous avons rapidement identifié deux types d’actions qui doivent être effectuées sur l’infrastructure.
La première est la construction CI, qui doit toujours fournir un retour immédiat au développeur. Elle vérifie la conformité ainsi que l’intégrité du code et est généralement profondément impliquée dans le processus de développement. Ces tâches sont sensibles au temps parce que quelqu’un les attend habituellement.
La deuxième catégorie que nous avons identifiée est un peu différente. Avec la montée de dependabot et d’autres scanners de sécurité, nous avons vu de plus en plus de pipelines déclenchés par ces bots. Le problème, c’est que nous voulons exécuter le pipeline pour vérifier nos dépendances et maintenir notre base de code à jour, mais en même temps, personne n’attend ces pipelines. Donc, cela ne ferait aucune différence si ces pipelines étaient retardés.
Examinons donc la deuxième catégorie et voyons si nous pouvons construire quelque chose dans GitHub. Eh bien, GitHub permet à quiconque de connecter des runners auto-hébergés à n’importe quel projet (héberger vos propres runners). Si vous regardez le processus, il est relativement simple : télécharger le runner, le connecter à votre organisation ou dépôt puis exécuter le script shell. Il y a aussi un petit utilitaire qui transforme ce runner en service systemd, donc nous n’avons pas à démarrer et arrêter le service nous-mêmes.
En regardant la distribution des jobs, GitHub indique que le job en file d’attente est retenu pendant 24 heures. Dans ce laps de temps, le job doit être pris en charge ou il expirera. Donc, 24 heures est techniquement assez de temps pour attendre le lever du soleil, peu importe quand le job a été lancé.
Maintenant que cela est couvert, nous avons commencé à examiner notre configuration locale et comment nous pouvons atteindre une telle planification de capacité. Notre configuration actuelle ressemble à ceci.

Nous n’avons pas de stockage par batterie connecté, car cela rendrait tout le système plus coûteux et complexe.
Toutes les métriques, comme la production d’énergie du panneau solaire ou la consommation énergétique des serveurs, sont suivies par des appareils tasmota indépendants (CloudFree EU Smart Plug).
Alors nous avons tout connecté. Pour la commodité, nous avons installé Ubuntu 22.10 (le même utilisé pour le runner hébergé GitHub) sur nos machines. Nous avons également installé la chaîne d’outils dont nous avions besoin, comme rustup, gcc-musl, protobuf.
Maintenant, nous avons écrit 3 services systemd indépendants.
Le premier service tourne en permanence et lit la production d’énergie depuis HomeAssistant (c’est là que nos données énergétiques sont agrégées). Il prend également en compte quels autres appareils fonctionnent actuellement et combien d’énergie ils consomment déjà. Il implémente ensuite le modèle d’état suivant :

Définition du service 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 (version abrégée)
#!/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 (solaire : $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 jusqu'à demain (10h)"
rtcwake -m disk -s 36000
fi
if [ $SALDO -gt 70 ]; then
echo "plus de 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
Ce service ne maintient que l’état selon lequel de l’énergie bon marché est disponible. Donc quand ce service systemd tourne, cela signifie qu’il y a de l’énergie disponible, quand il est arrêté, tous les workers doivent s’arrêter.
Nous utilisons donc ce service comme proxy pour faciliter la gestion.
[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
Le script que nous exécutons ici est simplement une commande sleep.
#!/bin/bash
sleep infinity
Nous avons suivi les instructions fournies par GitHub et installé le runner en tant que service systemd.
sudo ./svc.sh install
Cela nous a déjà donné la définition correcte du service et la seule chose que nous avons dû changer était la ligne de dépendance du service. Car maintenant, nous voulons que ce service tourne lorsque le service cheap-energy tourne, et aussi qu’il s’arrête lorsque le service cheap-energy est arrêté.
Nous avons donc modifié notre définition de service (actions.runner.DownToZero-Cloud.dtz-edge1.service) pour inclure la description 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
Maintenant que nous avons la partie hardware de la solution configurée, revenons au côté GitHub.
Nous avons maintenant 2 types de runners dans notre interface GitHub. L’un est le runner hébergé par GitHub, sur lequel nous voulons exécuter nos tâches de type 1, et l’autre est notre pool dtz-edge qui ne se lance que lorsqu’il y a assez d’énergie solaire.

Divisons nos définitions de pipeline.
Pour les jobs de type 1, tout peut rester comme dans un pipeline GitHub normal.
name: build
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
Pour les jobs de type 2, donc ceux que nous voulons exécuter avec délai sur les machines alimentées par solaire, il suffit de définir la section on-trigger pour inclure les scénarios à supporter ici. Dans notre cas, nous avons commencé par le faire pour toutes les pull-requests. Ensuite, la seule chose à changer est la déclaration runs-on. Ici nous avons placé notre runner nouvellement créé.
name: pr
on:
workflow_dispatch:
pull_request:
jobs:
test:
name: coverage
runs-on: self-hosted
Ainsi, chaque fois que dependabot nous envoie des mises à jour à fusionner, ou qu’un autre bot veut vérifier les tests et la couverture du code, ces jobs s’exécuteront dès que nous aurons les ressources pour le faire.
En bonus, nous n’avons plus non plus à payer pour ces runners supplémentaires. Les runners sur site sont gratuits (dans le sens des tarifs GitHub).