Plus notre petit projet avance, plus nous produisons de code. Et comme la plupart de notre communauté aujourd’hui, nous hébergeons notre code sur GitHub.
Ainsi, lors du 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.
Le premier est la build 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 car quelqu’un les attend généralement.
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 est que nous voulons exécuter le pipeline pour vérifier nos dépendances et garder 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.
Alors, regardons la deuxième catégorie et voyons si nous pouvons construire quelque chose à l’intérieur de GitHub. Eh bien, GitHub permet à quiconque d’attacher des runners auto-hébergés à n’importe quel projet (héberger vos propres runners). Si vous regardez le processus, c’est relativement simple, téléchargez le runner, attachez-le à votre organisation ou repo et lancez le script shell. Il y a aussi un petit outil qui transforme ce runner en service systemd, de sorte que nous n’ayons pas à démarrer et arrêter le service nous-mêmes.
Concernant la distribution des tâches, GitHub indique que la tâche en file d’attente est maintenue pendant 24 heures. Dans ce délai, la tâche doit être prise en charge sinon elle expirera. Donc 24 heures est techniquement un temps suffisant pour attendre que le soleil se lève, peu importe quand la tâche a été lancée.
Avec cela couvert, nous avons commencé à regarder notre configuration locale et comment nous pouvons réaliser une telle planification de capacité. Notre configuration actuelle ressemble à ceci.
Nous n’avons aucun stockage par batterie, car cela rendrait le système plus cher et complexe.
Toutes les métriques, comme la production d’énergie des panneaux solaires ou la consommation énergétique des serveurs, sont suivies par des appareils tasmota indépendants (CloudFree EU Smart Plug).
Nous avons donc tout connecté. Pour plus de commodité, nous avons installé Ubuntu 22.10 (le même utilisé pour le runner hébergé par GitHub) sur nos machines. Nous avons aussi installé la chaîne d’outils nécessaire, 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 aussi en compte quels autres appareils sont actuellement en marche et combien d’énergie ils consomment déjà. Il implémente 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 raccourcie)
#!/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 "Solde : $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 "dormir 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 porte uniquement l’état que de l’énergie pas chère est disponible. Donc quand ce service systemd tourne, cela signifie qu’il y a de l’énergie disponible, lorsqu’il est arrêté, tous les workers devraient s’arrêter.
Nous utilisons donc ce service comme un proxy afin de 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 lançons ici est simplement une commande sleep.
#!/bin/bash
sleep infinity
Nous avons suivi les instructions fournies par GitHub et installé le runner comme un 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 uniquement lorsque le service cheap-energy tourne, et aussi qu’il s’arrête lorsque cheap-energy s’arrête.
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 la partie matérielle de la solution est configurée, revenons à la partie 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 que notre tâche de type 1 s’exécute, 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 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, ceux que nous voulons exécuter en différé sur les machines alimentées par énergie solaire, il suffit de définir la section on
-trigger pour inclure les scénarios à supporter ici. Dans notre cas, nous avons commencé par faire cela pour toutes les pull requests. Ensuite, la seule chose à changer est l’instruction runs-on
. Ici nous plaçons notre runner nouvellement généré.
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 de code, ces jobs s’exécuteront dès que nous aurons les ressources pour le faire.
En bonus supplémentaire, nous n’avons plus à payer pour ces runners supplémentaires. Les runners on-premise sont gratuits (au sens de la tarification GitHub).