Els teus scripts ja funcionen. Però «funcionar» és el que fan al migdia, amb el teu terminal obert, les teves variables carregades i tu mirant la sortida. Producció és una altra cosa: són les 4:20 de la matinada, no hi ha ningú al davant, el PATH és el pobre de cron, el disc està més ple que ahir, la xarxa va estranya i l'única prova del que va passar serà el que el teu script hagi deixat escrit. Aquesta lliçó tanca el mòdul amb la llista de comprovació que salva aquella distància i amb el projecte que la demostra: desplegar.sh, l'script de desplegament de Tramontana Reserves, amb validació, còpia prèvia, canvi atòmic i reversió automàtica si l'aplicació no respon. És l'examen final del mòdul, i fa servir absolutament tot el que has après.
Contingut
- La llista de comprovació de producció
- Estructura canònica i
main "$@" - Configuració: la precedència, implementada
- Secrets
- Registre: què, com i on
- Sortida per a humans i per a màquines
- Bloqueig, temps límit i solapament
- Notificacions: silenci si tot va bé
- Proves, versionatge i documentació
- Quan deixar Bash
- Projecte:
desplegar.sh
- La llista de comprovació de producció
| Requisit | Per què |
|---|---|
| Rutes absolutes, sense dependre del directori de treball | Cron arrenca a $HOME amb un PATH mínim |
Mode estricte i trap de neteja |
Ningú no recollirà els temporals per tu |
Idempotent i amb --dry-run |
Per poder rellançar-lo després d'una fallada sense pensar-hi |
| Bloqueig i temps límit | Dos alhora, o un de penjat, fan més mal que cap |
| Registre amb data, decisions i durada | És l'única prova del que va passar |
| Codis de sortida documentats | Qui el crida ha de poder actuar sense llegir-ne la sortida |
| Sense secrets al codi ni a la línia d'ordres | ps els ensenya a qualsevol |
| Silenci si tot va bé | Una alerta que sona diàriament deixa de llegir-se |
shellcheck net i provat en un entorn de proves |
Una fallada de sintaxi a les 4:20 no l'arregla ningú |
Versionat a git, amb --help i un README |
D'aquí a un any no recordaràs per què |
Cap no és opcional en un script que corre sol: els deu junts són la diferència entre una automatització i una bomba de rellotgeria.
- Estructura canònica i
main "$@"
main "$@"La plantilla completa, evolució de la de 04-01. Copia-la tal qual:
#!/usr/bin/env bash
#
# nom.sh - Què fa, en una línia.
# Autor : Nom <correu> Data: 2026-08-18 Versió: 1.0.0
# Ús : nom.sh [-h] [-v] [-q] [-n] <argument>
# Sortida: 0 ok | 2 ús incorrecte | 69 servei no disponible | 75 ja en marxa
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"
# --- Constants -----------------------------------------------------------
readonly VERSIO_SCRIPT="1.0.0"
readonly BLOQUEIG="/var/lock/nom.lock"
LLINDAR=80 # configuració: 1. valor per defecte
# --- Funcions ------------------------------------------------------------
us() { ... }; validar() { ... }; feina() { ... }
# --- Punt d'entrada ------------------------------------------------------
main() {
carregar_config # 2. fitxer
analitzar_opcions "$@" # 4. arguments (l'entorn, 3, va amb les constants)
validar; feina
}
main "$@" # <- l'ÚLTIMA línia del fitxermain al final permet que el fitxer es llegeixi de dalt a baix com un text: primer les constants, després les peces, i al final el resum del que fa el programa. Com que Bash necessita que les funcions estiguin definides abans de cridar-les, main "$@" ha de ser l'última línia, i posar-la allà protegeix a més d'una fallada real: si el fitxer es trunca en copiar-lo, Bash no executarà un script a mitges, perquè la crida a main haurà desaparegut amb la resta. I va amb cometes, sempre, pel que saps de 04-03: és l'única manera que els arguments arribin intactes.
- Configuració: la precedència, implementada
La regla de 04-03 —arguments > entorn > fitxer > defectes— s'implementa aplicant-la en aquell ordre, de la més feble a la més forta:
LLINDAR_DISC=80 # 1. defecte
carregar_config() { # 2. fitxer
local f="${CONFIG:-/etc/tramontana/desplegar.conf}" clau valor
[[ -r $f ]] || return 0
while IFS='=' read -r clau valor; do
clau="${clau// /}"
[[ -z $clau || $clau == \#* ]] && continue
case "$clau" in
llindar_disc) LLINDAR_DISC="$valor" ;;
url_salut) URL_SALUT="$valor" ;;
*) log "clau desconeguda a $f: $clau" ;;
esac
done < "$f"
}
LLINDAR_DISC="${TRAMONTANA_LLINDAR_DISC:-$LLINDAR_DISC}" # 3. entorn
# 4. arguments: el bucle de getopts, que s'executa després${VAR:-$ACTUAL} pren el valor de l'entorn si existeix i, si no, conserva el que ja hi hagués: aquell petit truc és tot el mecanisme. I registra les claus desconegudes en comptes d'ignorar-les, perquè un llindar_dsic mal escrit que no diu res és una nit perduda.
- Secrets
Tres regles, en ordre d'importància:
- Mai a l'script. Un fitxer versionat a git amb una contrasenya a dins està compromès per sempre, encara que l'esborris: queda a l'historial.
- Mai a la línia d'ordres.
mysql -p"$CLAU"és visible aps auxper a qualsevol usuari de la màquina mentre duri l'ordre. Sí en un fitxer amb permisos 600, propietat de l'usuari del servei, o en una variable d'entorn del gestor de serveis.
llegir_secret() { # llegir_secret RUTA -> imprimeix el secret
local f="$1" mode
[[ -r $f ]] || morir 78 "no puc llegir el fitxer de secrets: $f"
mode=$(stat -c '%a' "$f") # 600 o 640, mai llegible per altres
[[ $mode == 600 || $mode == 640 ]] || morir 78 "permisos insegurs ($mode)"
< "$f" tr -d '\n'
}Un mínim digne: res de contrasenyes al codi i verificació activa dels permisos. La gestió seriosa —pass, un gestor de secrets, certificats TLS— és matèria de 06-05.
- Registre: què, com i on
Un script de producció registra cinc coses: que ha començat (amb la seva versió i els seus paràmetres), cada decisió que pren, cada canvi que fa al sistema, que ha acabat, i quant ha trigat. Amb això es reconstrueix qualsevol incident.
readonly LOG="/var/log/tramontana/desplegament.log"
registrar() { # registrar NIVELL MISSATGE...
local nivell="$1" linia; shift
linia="$(date '+%Y-%m-%dT%H:%M:%S%z') [$nivell] ${0##*/}[$$]: $*"
printf '%s\n' "$linia" >> "$LOG"
(( TRAMONTANA_SILENCIOS )) || printf '%s\n' "$linia" >&2
}
# I així queda el registre:
2026-08-18T14:02:11+0200 [INFO] desplegar.sh[4471]: inici v1.0.0, versio=3.3.0
2026-08-18T14:02:19+0200 [WARN] desplegar.sh[4471]: salut KO, revertint
2026-08-18T14:02:23+0200 [INFO] desplegar.sh[4471]: fi codi=69 durada=12sTres detalls del format. La data ISO 8601 amb zona horària (%Y-%m-%dT%H:%M:%S%z) s'ordena alfabèticament igual que cronològicament i no és ambigua en el canvi d'hora. El PID entre claudàtors permet separar dues execucions entrellaçades. I el missatge va alhora al fitxer i a stderr, perquè serveixi igual a cron que a mà.
La durada es mesura amb SECONDS, la variable que Bash incrementa sola: SECONDS=0 en començar i $SECONDS en acabar. I una nota de frontera: al Mòdul 5 substituirem aquest fitxer pel journal, amb logger -t tramontana -p daemon.info com a pont i journalctl per consultar-lo (05-06), més logrotate perquè el fitxer no creixi sense fi.
- Sortida per a humans i per a màquines
Un script útil serveix dos públics: la Marta vol llegir, la monitoració vol analitzar. La solució no és triar, és oferir totes dues:
case "$FORMAT" in
text) LC_ALL=C printf '%-10s %s\n' "Estat:" "$estat" ;;
json) LC_ALL=C printf '{"estat":"%s","versio":"%s","durada":%d}\n' \
"$estat" "$versio" "$SECONDS" ;;
esacAmb --quiet se suprimeix tot el que és informatiu i només queden els errors —imprescindible per a cron, que envia per correu qualsevol sortida— i amb --json la sortida és una sola línia analitzable. Si el teu JSON comença a tenir estructures imbricades, aquell és el senyal de l'apartat 10: en Bash no hi ha manera decent d'escapar cometes dins de valors arbitraris.
- Bloqueig, temps límit i solapament
Ja tens el flock sobre el descriptor 9 de 04-06. Falta l'altra meitat: què passa si una execució triga més que l'interval entre execucions. La còpia nocturna es llança a les 04:20; si un dia triga 25 hores, la de demà trobarà el bloqueig. Tres respostes, en ordre de preferència:
- Abandonar amb un codi propi (
75) i un avís. És el correcte per a còpies i informes: la de demà ja ho farà. - Esperar amb
flock -w 300 9, si el solapament és breu i esperar és acceptable. I mai matar l'anterior des de la nova: si cal això, el problema és l'interval, no el bloqueig.
A l'execució cal posar-li sostre, perquè un procés penjat no falla: es queda allà, ocupant el bloqueig per sempre.
# 20 minuts de límit; -k 30 envia SIGKILL si als 30 s no ha mort.
timeout -k 30 20m tar -czf "$arxiu" -C "$TEMPORAL" . \
|| morir 73 "la compressió ha superat el temps límit"timeout retorna 124 quan talla per temps, cosa que permet distingir-ho d'una fallada normal. És l'aplicació directa de la regla SIGTERM → esperar → SIGKILL de 03-06.
- Notificacions: silenci si tot va bé
El principi és curt i s'incompleix sempre: un script que avisa quan tot va bé ensenya la gent a ignorar els seus avisos. Si la Marta rep cada matí un correu de «còpia correcta», a la tercera setmana l'arxiva sense llegir, i el dia que hi posi «còpia FALLIDA» tampoc no el llegirà. La regla operativa: notifica només quan hi hagi alguna cosa a fer, i que el missatge digui què ha passat, quin impacte té i què s'espera de qui el llegeix.
notificar() { # notificar ASSUMPTE COS
(( NOTIFICAR )) || return 0
printf '%s\n\nServidor: %s\nRegistre: %s\n' "$2" "$(hostname)" "$LOG" \
| mail -s "[Tramontana] $1" "$DESTINATARI"
}
esperar_salut || { notificar "Desplegament revertit" \
"La versió ${versio} no ha respost al 8080; s'ha restaurat ${previ}."; }Perquè el silenci no es confongui amb «l'script no s'ha executat», la contrapartida és una marca d'èxit: l'última línia del registre amb la data d'avui, o un fitxer ULTIM_EXIT amb la marca de temps. Comprovar «la marca és d'avui?» és trivial i detecta el 90 % dels problemes, inclòs el pitjor: que la tasca hagi deixat d'executar-se.
- Proves, versionatge i documentació
Abans que un script toqui producció:
shellchecksense avisos ibash -n, tots dos obligatoris a CI:find scripts -name '*.sh' -exec shellcheck {} +.--dry-runcomplet contra el sistema real, llegint la sortida sencera: és l'última oportunitat de veure el que passarà. I en un entorn de proves amb les mateixes dades i rutes, amb instantània de la VM abans de cada prova destructiva.- El camí de fallada, no només el d'èxit: treu permisos al destí, apunta la URL de salut a un port tancat, interromp amb Ctrl+C a mitges. Si només ho has provat quan tot va bé, no ho has provat.
- Amb l'entorn de cron, que és diferent del teu:
env -i /home/operador/scripts/desplegar.sh -n 3.3.0 /tmp/app.tgzreprodueix la pobresa de variables que tindrà a les 4:20.
El versionatge no és opcional: ~/scripts és un repositori git, amb un commit per canvi i un missatge que digui per què. Desplegar l'script és aleshores git pull al servidor —o millor, copiar-lo amb rsync des d'un repositori central, sense deixar credencials de git al servidor—. I la documentació mínima són tres coses: la capçalera amb ús i codis de sortida, un --help que funcioni sempre, i un README.md a la carpeta que digui quin script fa què i quin crida quin. Res més; res menys.
- Quan deixar Bash
Bash és excel·lent per orquestrar ordres. Deixa de ser-ho quan apareix qualsevol d'aquests senyals:
| Senyal | Cap a on |
|---|---|
Més de ~300 línies, o un main que no cap en una pantalla |
Python |
| Estructures de dades imbricades, JSON més enllà de llegir un camp | Python (json), o jq si és només consulta |
| Càlcul numèric, dates complicades, informes | Python |
| Concurrència real amb coordinació entre tasques | Python, Go |
| Configurar màquines, i més d'una | Ansible (07-06) |
| Dependències entre passos, estat entre execucions | Una eina d'orquestració |
El senyal més honest: si estàs escrivint eval, o escapant cometes dins de cometes dins de cometes, ja has passat el límit. Per a Tramontana la frontera natural és Ansible: en el moment en què el desplegament s'hagi de fer en tres servidors alhora, desplegar.sh es converteix en un rol d'Ansible, i aquest mòdul haurà complert la seva funció, que era ensenyar-te exactament què fa aquell rol per sota.
- Projecte:
desplegar.sh
desplegar.shL'examen final del mòdul. Valida, comprova precondicions, copia abans de tocar, extreu, verifica, canvia l'enllaç de manera atòmica, comprova amb curl i reverteix sol si l'aplicació no respon.
#!/usr/bin/env bash
#
# desplegar.sh - Desplegament de Tramontana Reserves amb reversió automàtica.
# Autor : Operador de sistemes <operador@srv-tramontana> Data: 2026-08-18
# Ús : desplegar.sh [-h] [-v] [-q] [-n] [-t SEG] <versio> <paquet.tar.gz>
# Sortida: 0 ok | 2 ús | 65 paquet invàlid | 66 origen il·legible | 73 no es
# pot escriure | 69 l'app no ha respost (revertit) | 75 ja en marxa
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"
readonly VERSIO_SCRIPT="1.0.0"
readonly BASE="/opt/tramontana"; readonly RELEASES="${BASE}/releases"
readonly BLOQUEIG="/var/lock/tramontana-desplegament.lock"
readonly LOG="/var/log/tramontana/desplegament.log"
URL_SALUT="${TRAMONTANA_URL_SALUT:-http://10.0.2.15:8080/salut}"
ESPERA=60; DRY_RUN=0
versio=""; paquet=""; previ=""; enllac_canviat=0
# registrar() i us() són les de l'apartat 5 i de 04-03, sense canvis.
revertir() { # desfà el canvi d'enllaç si es va fer
(( enllac_canviat )) || return 0
registrar WARN "revertint al release $previ"
( cd "$BASE" && ln -sfn "releases/${previ}" app )
enllac_canviat=0
}
netejar() {
local codi=$?
(( codi != 0 )) && revertir
registrar INFO "fi codi=${codi} durada=${SECONDS}s"
exit "$codi"
}
trap netejar EXIT
trap 'registrar ERROR "interromput per senyal"; exit 130' INT TERM
validar() {
[[ $versio =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || morir 2 "versió invàlida: $versio"
[[ -r $paquet ]] || morir 66 "no puc llegir el paquet: $paquet"
# tar -tzf abans d'extreure: convenció del curs des de 02-04.
tar -tzf "$paquet" >/dev/null 2>&1 || morir 65 "el paquet no és un tar.gz vàlid"
[[ -w $RELEASES ]] || morir 73 "no puc escriure a $RELEASES"
requereix_comanda tar curl timeout || morir 69 "falten dependències"
}
esperar_salut() { # 0 si respon 200 abans de $ESPERA segons
local fi=$(( SECONDS + ESPERA )) codi
while (( SECONDS < fi )); do
codi=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL_SALUT" || true)
[[ $codi == 200 ]] && return 0
sleep 3
done
return 1
}
main() {
SECONDS=0
while getopts ":hvqnt:" o; do
case "$o" in
h) us; exit 0 ;; n) DRY_RUN=1 ;; t) ESPERA="$OPTARG" ;;
v) TRAMONTANA_VERBOSE=1 ;; q) TRAMONTANA_VERBOSE=0 ;;
\?) us >&2; morir 2 "opció desconeguda: -$OPTARG" ;;
:) morir 2 "l'opció -$OPTARG necessita un valor" ;;
esac
done
shift $(( OPTIND - 1 ))
[[ $# -eq 2 ]] || { us >&2; morir 2 "calen <versio> i <paquet>"; }
versio="$1"; paquet="$2"; validar
exec 9>"$BLOQUEIG" || morir 73 "no puc obrir $BLOQUEIG"
flock -n 9 || morir 75 "ja hi ha un altre desplegament en marxa"
previ=$(basename "$(readlink -f "${BASE}/app")")
registrar INFO "inici v${VERSIO_SCRIPT}, versio=${versio}, previ=${previ}"
[[ $versio == "$previ" ]] && { registrar INFO "ja desplegada; res a fer"; exit 0; }
if (( DRY_RUN )); then
registrar INFO "SIMULACIÓ: extrauria $paquet a ${RELEASES}/${versio}"
registrar INFO "SIMULACIÓ: app -> releases/${versio}, salut a $URL_SALUT"
exit 0
fi
"${SCRIPT_DIR}/copia_tramontana.sh" -q >/dev/null \
|| morir 73 "la còpia prèvia ha fallat; abandono sense tocar res"
registrar INFO "còpia prèvia completada"
mkdir -p "${RELEASES}/${versio}" # idempotent
rm -rf -- "${RELEASES:?}/${versio:?}"/*
timeout -k 30 10m tar -xzf "$paquet" -C "${RELEASES}/${versio}"
[[ -s "${RELEASES}/${versio}/app.jar" ]] || morir 65 "el release no porta app.jar"
registrar INFO "release ${versio} extret ($(formatar_bytes \
"$(du -sb "${RELEASES}/${versio}" | cut -f1)"))"
# Canvi atòmic: ln -sfn amb destí relatiu, com mana el Mòdul 2.
( cd "$BASE" && ln -sfn "releases/${versio}" app )
enllac_canviat=1
registrar INFO "enllaç app -> releases/${versio}"
if esperar_salut; then
enllac_canviat=0 # confirmat: no cal revertir
registrar INFO "salut OK a ${URL_SALUT}"
printf '%s\n' "$versio" # stdout: la versió activa
exit 0
fi
registrar ERROR "la versió ${versio} no ha respost en ${ESPERA}s"
exit 69 # el trap EXIT farà la reversió
}
main "$@"operador@srv-tramontana:~$ ~/scripts/desplegar.sh 3.3.0 /tmp/app-3.3.0.tar.gz
2026-08-18T14:02:11+0200 [INFO] desplegar.sh[4471]: inici v1.0.0, versio=3.3.0, previ=3.2.1
2026-08-18T14:02:14+0200 [INFO] desplegar.sh[4471]: còpia prèvia completada
2026-08-18T14:02:16+0200 [INFO] desplegar.sh[4471]: release 3.3.0 extret (99.2 MiB)
2026-08-18T14:02:16+0200 [INFO] desplegar.sh[4471]: enllaç app -> releases/3.3.0
2026-08-18T14:03:16+0200 [ERROR] desplegar.sh[4471]: la versió 3.3.0 no ha respost en 60s
2026-08-18T14:03:16+0200 [WARN] desplegar.sh[4471]: revertint al release 3.2.1
2026-08-18T14:03:16+0200 [INFO] desplegar.sh[4471]: fi codi=69 durada=65s
operador@srv-tramontana:~$ readlink /opt/tramontana/app
releases/3.2.1El servei va continuar funcionant. Això és el que aquest mòdul ha construït: la 3.3.0 no va arrencar, l'script ho va detectar, va revertir a l'enllaç anterior en el mateix segon, ho va registrar tot i va retornar un codi (69) que diu exactament què va passar. I amb -n hauries vist abans el que anava a intentar, sense tocar res.
Repassa on viu cada lliçó: el shebang i l'exit explícit són de 04-01; ${VAR:-}, les matrius i printf de 04-02; getopts, us(), stdout enfront de stderr i els codis de sortida de 04-03; [[ =~ ]], case i el while d'espera de 04-04; SCRIPT_DIR, la biblioteca i morir() de 04-05; i set -euo pipefail, els trap, el bloqueig, la idempotència i timeout de 04-06. Res d'això no és nou: és tot l'anterior, junt.
Errors Comuns i Consells
- Rutes relatives. Funcionen des del teu terminal i fallen des de cron: absolutes sempre, o derivades de
SCRIPT_DIR. I no suposis el PATH, que a cron és/usr/bin:/bin; si fas servir alguna cosa de/usr/local/bin, ruta completa oPATH=explícit al principi. - Posar
main "$@"sense cometes o al mig del fitxer. El primer trenca els arguments amb espais; el segon falla perquè les funcions encara no estan definides. I no notifiquis l'èxit: ensenya a ignorar les notificacions. Silenci si tot va bé, i una marca d'èxit comprovable. - Registrar només el final. Sense les decisions intermèdies no pots reconstruir res; registra també el que vas decidir no fer. I posa sempre temps límit: un procés penjat no falla, es queda allà amb el bloqueig agafat fins que algú el mata.
- Desplegar sense
--dry-runprevi. Trenta segons de lectura contra mitja hora de restauració. - Consell: guarda
~/scriptsa git des d'avui encara que només siguis tu, perquègit logrespon a «des de quan fa això?» millor que la teva memòria. I fes que tot script imprimeixi la seva versió a la primera línia del registre: quan dos servidors es comportin diferent, serà el primer que compararàs. - Consell: el millor moment per escriure el
READMEde la carpeta és avui, quan encara recordes per què existeix cada script.
Exercicis
Exercici 1. Afegeix a desplegar.sh una opció --json que, en acabar, imprimeixi per stdout una línia amb versio, previ, estat (desplegat o revertit), codi i durada. Ha de funcionar també quan hi ha reversió, sense duplicar codi.
Exercici 2. Escriu ~/scripts/comprovar_tasques.sh, que verifiqui que les tres tasques nocturnes (cron-copia.log, cron-purga.log i el desplegament) han deixat marca d'avui i notifiqui només si en falta alguna, aplicant el principi de silenci si tot va bé.
Exercici 3. En Luis vol posar aquesta línia a /etc/cron.d/tramontana: 20 4 * * * operador cd ~/scripts && ./desplegar.sh $VERSIO /tmp/app.tar.gz. Troba-hi els cinc problemes i reescriu-la.
Solucions
Solució 1. La clau és no duplicar: el punt únic de sortida ja existeix —el trap netejar EXIT— i és allà on s'ha d'imprimir el resum.
FORMAT="text"; estat="desconegut"
# Al bucle d'opcions, que passa a manual per ser una opció llarga:
# --json) FORMAT="json"; shift ;;
resum() { # resum CODI
(( $1 == 0 )) && estat="desplegat" || estat="revertit"
case "$FORMAT" in
json) LC_ALL=C printf \
'{"versio":"%s","previ":"%s","estat":"%s","codi":%d,"durada":%d}\n' \
"$versio" "$previ" "$estat" "$1" "$SECONDS" ;;
*) printf '%s\n' "$versio" ;;
esac
}
# A netejar(), just abans de l'exit: resum "$codi"
operador@srv-tramontana:~$ ~/scripts/desplegar.sh --json 3.3.0 /tmp/app.tgz 2>/dev/null
{"versio":"3.3.0","previ":"3.2.1","estat":"revertit","codi":69,"durada":65}El 2>/dev/null deixa només la línia JSON, perquè el registre va a stderr: la separació de canals de 04-03 donant el seu fruit. I com que l'exit 0 del camí d'èxit també dispara l'EXIT, s'elimina el printf '%s\n' "$versio" del cos de main per no imprimir-lo dues vegades.
Solució 2.
#!/usr/bin/env bash
# comprovar_tasques.sh - Verifica que les tasques nocturnes han deixat marca d'avui.
# Autor : Operador de sistemes <operador@srv-tramontana> Data: 2026-08-18
# Ús : comprovar_tasques.sh [-v] Sortida: 0 tot al dia | 1 en falta alguna
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comuns.sh" # shellcheck source=lib/comuns.sh
readonly DESTINATARI="[email protected]"
declare -A TASQUES=( [copia]="/var/log/tramontana/cron-copia.log"
[purga]="/var/log/tramontana/cron-purga.log"
[desplegament]="/var/log/tramontana/desplegament.log" )
avui=$(date '+%Y-%m-%d'); falten=()
for tasca in "${!TASQUES[@]}"; do
fitxer="${TASQUES[$tasca]}"
# -s: existeix i no és buit. tail -1 evita llegir un registre de milers de línies.
if [[ -s $fitxer ]] && [[ $(tail -1 "$fitxer") == "$avui"* ]]; then
log "$tasca: al dia"
else falten+=("$tasca"); fi
done
(( ${#falten[@]} == 0 )) && exit 0 # silenci si tot va bé
printf "Sense marca d'avui (%s): %s\n" "$avui" "${falten[*]}" >&2
printf "Les tasques %s no han deixat registre d'avui a %s.\nRevisa cron i el disc.\n" \
"${falten[*]}" "$(hostname)" | mail -s "[Tramontana] Tasques sense executar" "$DESTINATARI"
exit 1
operador@srv-tramontana:~$ ~/scripts/comprovar_tasques.sh; echo "codi: $?"
codi: 0
operador@srv-tramontana:~$ ~/scripts/comprovar_tasques.sh -v; echo "codi: $?"
[2026-08-18 08:05:01] copia: al dia
[2026-08-18 08:05:01] purga: al dia
Sense marca d'avui (2026-08-18): desplegament
codi: 1Sense -v i amb tot correcte l'script no imprimeix res i retorna 0: cron no envia correu i la Marta no rep soroll. El correu només s'envia al camí de fallada, dient què passa i què cal revisar, com demana la convenció del curs.
Solució 3. Els cinc problemes:
~no s'expandeix en un crontab de sistema. El camp es passa ash -camb un entorn mínim iHOMEpot no estar definida: rutes absolutes.$VERSIOés buida. Cron no hereta el teu entorn, així que l'script rebria un argument on n'esperava dos i la validació ho rebutjaria. Sort que sí.- Falta redirigir la sortida. Sense
>> registre 2>&1, qualsevol missatge es converteix en un correu i qualsevol error es perd si el correu no està configurat. - Sense
SHELLniPATHdeclarats al principi del fitxer, i amb l'hora mal triada: les 04:20 eviten la franja del canvi horari i el solapament amb altres tasques. - Desplegar automàticament per cron és una mala idea de fons. Un desplegament l'aprova la Marta; el que ha de córrer sol és la còpia.
# /etc/cron.d/tramontana
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# Còpia diària. 04:20 per no solapar-se amb la rotació de registres.
20 4 * * * operador /home/operador/scripts/copia_tramontana.sh -q \
>> /var/log/tramontana/cron-copia.log 2>&1
# Comprovació que les tasques nocturnes han deixat marca. Només avisa si falla.
5 8 * * * operador /home/operador/scripts/comprovar_tasques.sh \
>> /var/log/tramontana/cron-copia.log 2>&1Rutes absolutes, PATH explícit, MAILTO="" perquè l'script ja escriu el seu propi registre i només notifica quan cal, i el desplegament fora de cron: es llança a mà quan la Marta l'aprova. Al Mòdul 5 veurem que un timer de systemd fa tot això amb millor registre i millor control de solapament.
Conclusió
Has tancat el mòdul, i amb ell la distància entre «a mi em funciona» i «corre sol».
- Tens una llista de comprovació de producció de deu punts i una plantilla canònica amb
main "$@"com a última línia, que fa que l'script es llegeixi de dalt a baix i no s'executi a mitges si es trunca. Implementes la precedència de configuració —arguments > entorn > fitxer > defectes— de debò, registrant fins i tot les claus desconegudes. - Mantens els secrets fora del codi i de la línia d'ordres, i verifiques els permisos del fitxer que els guarda.
- Registres inici, decisions, canvis, fi i durada, amb data ISO i PID, a fitxer i a stderr, sabent que a 05-06 això passarà al journal.
- Produeixes sortida per a humans i per a màquines, amb
--quieti--json. Controles el solapament ambflock, poses sostre ambtimeoutsabent que 124 significa «s'ha esgotat el temps», i apliques el silenci si tot va bé amb una marca d'èxit comprovable com a contrapartida. - Proves amb
shellcheck,bash -n,--dry-run, entorn de proves,env -ii el camí de fallada; versiones a git i documentes amb capçalera,--helpiREADME. - Reconeixes quan deixar Bash —300 línies, JSON imbricat, concurrència,
eval— i cap a on anar. Idesplegar.shexisteix: valida, bloqueja, copia abans de tocar, extreu amb temps límit, canvia l'enllaç de manera atòmica, comprova la salut i reverteix sol quan la 3.3.0 no arrenca, deixant el servei dret.
A /home/operador/scripts/ hi ha ara una caixa d'eines real: revisio_salut.sh, informe_reserves.sh, resum_reserves.sh, purgar_releases.sh, copia_tramontana.sh, comprovar_tasques.sh, desplegar.sh, la biblioteca lib/comuns.sh i el seu test_comuns.sh, tots amb la mateixa estructura, els mateixos codis de sortida i la mateixa disciplina d'errors. Això ja no és «saber Bash»: és saber automatitzar.
Però fixa't en el que ha anat apareixent entre línies i hem anat ajornant. copia_tramontana.sh escriu a /var/log/tramontana/ i ningú no rota aquell fitxer. desplegar.sh canvia l'enllaç però no reinicia l'aplicació, perquè encara no saps gestionar serveis. La còpia va a /srv/tramontana/backups sense que hàgim parlat de quant disc hi ha ni de què passa quan s'acabi. El grup tramontana continua sense crear-se formalment, svc-tramontana existeix per art de màgia, i sudo l'has fet servir sense preguntar-te qui decideix què hi pots fer. Al Mòdul 5: Administració del Sistema tot això deixa de ser màgia: crearàs usuaris i grups de debò, configuraràs sudo i els permisos especials, instal·laràs i fixaràs versions de paquets, gestionaràs discos i sistemes de fitxers, convertiràs els teus scripts en serveis i timers de systemd amb arrencada, dependències i reinici automàtic, enviaràs els teus registres al journal amb rotació, mesuraràs el rendiment del servidor, i tancaràs amb una estratègia de còpia i restauració digna d'aquest nom. Els scripts que has escrit seran les peces; el Mòdul 5 és el sistema que les sosté. Actualitza la instantània de la teva VM i ens veiem allà.
Curs de Linux: De Principiant a Administrador de Sistemes
Mòdul 1: Introducció a Linux
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
