Automatitzant l'actualització de la meva web amb un webhook
Com que els fitxers d’aquesta web estan a GitHub, vaig pensar que seria bona idea actualitzar-la automàticament amb cada canvi al repositori.
El meu sistema
Aquesta web se serveix des d’una instància ARM de la Free Tier d’Oracle[1] màquina virtual de Vultr barata i fiable.
La web està construïda amb Zola, un generador de llocs estàtics escrit en Rust. Per actualitzar-la, només cal sincronitzar el repositori i executar zola build
. Vaig escriure un senzill script en bash (update_osc.garden
) que fa exactament això:
#!/usr/bin/env bash
repo="/opt/osc.garden/repo"
out_dir="/var/www/osc.garden"
Vaig fer que l’arxiu fos executable amb chmod +x update_osc.garden
.
Podria executar aquest script manualment cada cop que fes canvis a la web, però automatitzar les coses és molt més divertit.
Opcions
La meva primera idea va ser utilitzar cron
per programar una tasca que executés l’script cada pocs minuts. Bastant avorrit i poc eficient —per què reconstruir la web si no hi ha hagut canvis?
Vaig demanar idees a GPT-4. Va suggerir: GitHub Actions, webhooks, cron, SFTP manual i serveis de «continuous deployment».
Cron ja estava descartat. SFTP és encara pitjor que cron, i no puc utilitzar serveis de «continuous deployment» o GitHub Actions perquè allotjo la web en un servidor propi.
Els webhooks sonaven a màgia negra, una mica com les notificacions push. Hora d’aprendre!
Webhooks
Un webhook és una forma de dir-li a un servidor «Ei! Ha ocorregut! La cosa que t’interessava, ha ocorregut! Fes la teva». En altres paraules, cada cop que faci un canvi al repositori, GitHub donarà un toc al servidor webhook perquè actualitzi la web.
Vaig instal·lar un servidor de webhook lleuger escrit en Go amb sudo apt install webhook
.
A continuació, vaig crear hooks.yaml
definint el hook que executarà l’script que ja tenia:
- id: "update-osc.garden"
execute-command: "/opt/osc.garden/update_osc.garden"
command-working-directory: "/
Ara puc iniciar el servidor webhook amb webhook -hooks hooks.yaml -verbose
. Abans de poder utilitzar-lo, però, he d’obrir els ports.
Obrint els ports
Tallafoc local
Utilitzo ufw per gestionar el tallafoc local. Vaig executar sudo ufw allow 9000/tcp
per permetre connexions TCP al port 9000.
Però això no era prou; Oracle té el seu propi tallafoc. Sempre oblido com obrir els seus ports, així que aquesta vegada ho he apuntar, per al meu jo del futur:
Tallafoc d’Oracle
-
Inicia sessió a la teva consola d’Oracle Cloud Infrastructure.
-
Ves a Dashboard → Virtual cloud networks. Obre el teu VCN.
-
Clica en el nom de la teva «subnet» (subxarxa).
-
Clica en la llista de seguretat de la teva subxarxa.
-
Clica en «Add Ingress Rules» i omple els camps. En aquest cas:
- Source Type: CIDR
- Source CIDR: 140.82.112.0/20 (vaig afegir una regla per cada bloc CIDR IP de la secció
hooks
de les IPs de GitHub) - Source Port Range: Tots
- Destination Port Range: 9000
- Description: GitHub webhook
Fet!
Accés segur
Utilitzant certificats SSL
HTTP és cosa del passat. Volia emprar HTTPS, així que vaig haver de modificar la comanda per utilitzar els meus certificats:
Així puc connectar-me a https://osc.garden:9000/hooks/update-osc.garden
per activar l’script:
)
> Successfully )
)
Fantàstic. Ara toca assegurar i automatitzar.
Accés exclusiu a GitHub
Cal actualitzar l’arxiu YAML per afegir una «regla de disparador» perquè només faci cas a les sol·licituds autoritzades.
- id: "update-osc.garden"
execute-command: "/opt/osc.garden/update_osc.garden"
command-working-directory: "/opt/osc.garden"
trigger-rule:
and:
- match:
type: "payload-hmac-sha256"
secret: "my-super-secret-token"
parameter:
source: "header"
name: "X-Hub-Signature-256"
Aquesta regla conté un secret
, que és una contrasenya que he creat.
Ara, si intento activar el webhook sense la contrasenya, webhook
es queixa: Hook rules were not satisfied.
Genial: només acceptarà les sol·licituds que continguin el secret
xifrat.
És hora de configurar el webhook de GitHub.
Configuració del webhook de GitHub
Vaig anar al meu repositori → Settings → Webhooks → Add webhook, i vaig introduir:
- Payload URL:
https://osc.garden:9000/hooks/update-osc.garden
- Content type:
application/json
- Secret:
my-super-secret-token
- Enable SSL verification
- Trigger events: «Només l’esdeveniment
push
».
Vaig clicar «Add webhook» i vaig fer un canvi en el repositori. Funciona! GitHub va notificar al meu servidor webhook i la meva web es va reconstruir amb els canvis.
Ja quasi estem. Falta executar el servidor webhook amb un usuari que no sigui root i crear un servei perquè sempre estigui actiu.
Servei del servidor webhook
Aïllament i permisos
No és bona idea executar el servidor webhook com a root, així que vaig crear un nou usuari per executar-lo amb sudo adduser webhookuser
. A continuació, vaig afegir aquest usuari a un nou grup sslcerts
, i li vaig donar els permisos necessaris:
# Estableix webhookuser com a propietari dels directoris del projecte i de la web.
# Crea grup sslcerts i afegeix a webhookuser.
# Atorga permisos de lectura i execució a sslcerts per als certificats.
# Habilita el recorregut de directoris per a 'altres' en els directoris de certificats.
# Configura els permisos dels fitxers i el grup per als certificats SSL.
La idea és minimitzar els permisos per reduir el dany potencial d’una mala configuració o una vulnerabilitat de seguretat.
Aquí em vaig trobar amb un problema en intentar executar l’script com a webhookuser
: Zola intenta eliminar el directori abans de construir el lloc web —els permisos anteriors no són suficients.
Podria donar a webhookuser
permisos sobre /var/www
, però vaig optar per utilitzar un directori temporal per zola build
:
#!/usr/bin/env bash
repo="/opt/osc.garden/repo"
live_dir="/var/www/osc.garden"
# Crea un directori temporal.
temp_dir=
# Assegura la neteja en sortir (exitosament o no).
# Actualitza el repositori i els submòduls.
# Construeix el lloc al directori temporal.
# Sincronitza els arxius al directori en viu.
Servei de systemd
Systemd és el primer procés que s’executa a Debian, inicialitzant la configuració del sistema i gestionant les tasques en segon pla («serveis» o «daemons»). Vaig decidir utilitzar un fitxer de servei de systemd per automatitzar l’inici del servidor de webhook, afegir registres (logging), controlar encara més els seus privilegis i reiniciar-lo en cas que falli.
Vaig crear el fitxer de servei amb permisos mínims amb sudo vim /etc/systemd/system/webhook.service
:
# Seguretat.
Després de desar el fitxer, vaig recarregar el daemon i vaig iniciar el servei:
Puc veure com està funcionant el servei amb sudo systemctl status webhook
i sudo journalctl -fu webhook
.
Després de comprovar que funcionava, vaig configurar el servei perquè s’iniciés automàticament en arrencar amb sudo systemctl enable webhook.service
.
L’script final
Vaig afegir registres, missatges d’error i la meva funcionalitat preferida: notificacions push pel mòbil amb ntfy
:
#!/usr/bin/env bash
repo="/opt/osc.garden/repo"
live_dir="/var/www/osc.garden"
ntfy_url="ntfy.osc.garden/builds"
ntfy_token="my-ntfy-access-token"
# Crea un directori temporal.
temp_dir= ||
# Garanteix la neteja en sortir (amb èxit o no).
# Actualitza el repositori i els submòduls.
||
||
||
||
# Construeix el lloc en el directori temporal.
||
# Minifica l'HTML amb https://github.com/terser/html-minifier-terser
# La minificació nativa de Zola elimina les cometes necessàries perquè les targetes de xarxes socials funcionin de forma consistent a WhatsApp.
||
# Sincronitza els arxius al directori final.
||
# Elimina la trampa i mostra un missatge d'èxit.
Les notificacions es veuen així (fes clic per alternar entre èxit i error):
Això és tot! Després d’experimentar amb noves eines, barallar-me amb els permisos i aprendre força, la meva web s’actualitza automàgicament cada cop que el repositori canvia. 🎉🥳
Extra: Sobre les notificacions push i els webhooks
Després d’aprendre sobre la màgia negra dels webhooks, vaig investigar una mica sobre les notificacions push —efectivament, hi ha similituds. Els mòbils, però, en lloc d’escoltar en un port, depenen de connexions duradores a un servei de notificacions. Utilitzen un mecanisme de keep-alive per mantenir la connexió oberta i quan el servidor té alguna cosa per notificar, envia la notificació a través d’aquesta connexió preexistent.
Com a analogia: els webhooks són com els teus amics enviant-te un missatge de text (una nova connexió cada cop) per dir-te alguna cosa. Les notificacions push, en contraposició, són com estar en una llarga trucada de veu amb un amic —inclús quan ningú parla, la connexió persisteix. Quan algú té alguna cosa a dir, ho fa a través de la trucada existent en lloc d’establir una nova connexió. El mecanisme de keep-alive —que manté la connexió oberta— seria com preguntar, després d’uns minuts de silenci, «Hi ets?». «Hi soc».
-
Oracle va eliminar el meu compte de Free Tier sense avís, explicació o recurs (una pràctica habitual, sembla). Ara estic allotjant el lloc en una instància de Vultr assequible i fiable que tinc des de fa més de sis anys. Si vols provar Vultr, aquí tens el meu enllaç de registre de referència, que et dona $100 en crèdit, mentre jo rebo $10. ↩