A 07-03 va néixer copia.sh: copiava /srv/veloz/dades amb rsync --link-dest, comprimia, verificava amb sha256sum i aplicava una retenció senzilla. N'hi havia prou per no perdre les dades del dia, però no per dormir tranquil. Aquest projecte el porta a la seva versió definitiva i digna de confiança: perfils configurables sense tocar el codi, manifest verificable, retenció amb promoció a setmanal i mensual, còpia remota, xifratge opcional, esborrat segur i —el més important— una prova de restauració automàtica que s'executa sola cada setmana. El principi que ordena tot el projecte: una còpia de seguretat no verificada no existeix.
Contingut
- Inventari: què es copia, amb quin RPO i RTO
- La regla 3-2-1 aplicada a Veloz Envíos
- Disseny: perfils en fitxers, no en el codi
- Fase 1: la còpia incremental que es restaura com a completa
- Fase 2: manifest i verificació
- Fase 3: retenció amb promoció
- Fase 4: restauració i la seva prova automàtica
- Còpia remota i xifratge
- Espai en disc: comprovar abans de començar
- Observabilitat i codis de sortida
- Posada en producció i runbook
- Inventari: què es copia, amb quin RPO i RTO
L'inventari s'escriu abans que el codi, i es revisa cada trimestre:
| Conjunt | Camí | Mida | RPO | RTO | Per què |
|---|---|---|---|---|---|
| Dades operatives | /srv/veloz/dades |
6 GB | 1 dia | 1 h | Reconstruir-les exigiria demanar els CSV als clients |
| Històric | /srv/veloz/dades/historic |
40 GB | 1 mes | 1 dia | Immutable; només canvia en tancar el mes |
| Configuració | ~/veloz-ops/etc, /etc/veloz |
2 MB | 1 dia | 15 min | Petit i crític: sense ell no arrenca res |
| Sistema operatiu | — | — | — | — | No es copia: es reinstal·la des de la recepta |
RPO és quantes dades acceptem perdre; RTO, quant triguem a tornar. Les dues columnes són les que decideixen la freqüència i el disseny, i l'última fila és la més important: copiar el sistema sencer multiplica el cost i no millora cap dels dos números.
- La regla 3-2-1 aplicada a Veloz Envíos
Tres còpies, dos mitjans, una a fora (07-03):
| Còpia | On | Com | Freqüència |
|---|---|---|---|
| 1 (les dades vives) | srv-veloz-01:/srv/veloz/dades |
— | — |
| 2 | srv-veloz-01:/copies |
rsync --link-dest |
Diària, 03:15 |
| 3 | srv-veloz-03:/copies/srv-veloz-01 |
rsync -e ssh |
Diària, després de la còpia 2 |
| A fora | Emmagatzematge extern | tar + gpg -c mensual |
Mensual, dia 1 |
La còpia 2 al mateix disc que les dades no compleix la regla per si sola —un disc que mor se les endú totes dues— però és la que dóna un RTO d'una hora, perquè restaurar de local és instantani. La 3 cobreix la mort del servidor; la de fora, l'incendi i l'esborrat maliciós.
- Disseny: perfils en fitxers, no en el codi
L'error de 07-03 era tenir els camins dins de l'script: afegir un conjunt exigia editar codi, i editar codi exigeix revisió i desplegament. La versió definitiva llegeix perfils d'etc/copia.d/*.conf:
# etc/copia.d/10-dades.conf
NOM=dades
ORIGEN=/srv/veloz/dades/
DESTI=/copies
EXCLUSIONS=/tmp/*:*.swp:historic/
RETENCIO_DIARIES=7; RETENCIO_SETMANALS=4; RETENCIO_MENSUALS=6
VERIFICA_GLOB='*.csv'El prefix numèric fixa l'ordre. Es carreguen en bucle, cadascun en una subshell perquè les variables d'un perfil no contaminin el següent (01-04):
for perfil in "$ETC/copia.d"/*.conf; do
[[ -r $perfil ]] || continue
( set -a; . "$perfil"; set +a
copia_perfil ) || fallades+=("$(basename "$perfil")")
doneset -a marca per a exportació tot el que es defineixi a continuació, i la subshell aïlla. L'alternativa —carregar-ho tot en el mateix àmbit— provoca el bug clàssic: el perfil 20 hereta la RETENCIO_MENSUALS del 10 perquè es va oblidar de definir-la. Amb la subshell, cada perfil arrenca dels valors per defecte de l'script, que és la precedència de 05-06: defecte < fitxer < entorn < opcions.
- Fase 1: la còpia incremental que es restaura com a completa
copia_perfil() {
local avui previa enllac=() excl
avui=$(date +%F)
previa=$(ultima_correcta "$DESTI/$NOM")
[[ -n $previa ]] && enllac=(--link-dest="$DESTI/$NOM/$previa")
excl=$(mktemp); trap 'rm -f "$excl"' RETURN
printf '%s\n' ${EXCLUSIONS//:/ } > "$excl"
rsync -a --delete --exclude-from="$excl" "${enllac[@]}" ${SIMULACIO:+--dry-run} \
"$ORIGEN" "$DESTI/$NOM/$avui.parcial/" || return 1
mv "$DESTI/$NOM/$avui.parcial" "$DESTI/$NOM/$avui"
}Quatre decisions que valen per tot el projecte. --link-dest (07-03) enllaça dur el que no ha canviat: la còpia d'avui ocupa el que ocupen els fitxers nous, però conté l'arbre complet, així que restaurar és copiar un directori i no reconstruir una cadena —amb el requisit que origen i destí dels enllaços siguin al mateix sistema de fitxers—. I enllac=() és un array (04-03), no una cadena: el primer dia no hi ha còpia prèvia, l'array queda buit i l'opció desapareix de l'ordre; si fos una cadena buida entre cometes, rsync rebria un argument buit i fallaria.
${SIMULACIO:+--dry-run} (03-06) afegeix l'opció només si la variable té valor. El mode simulació és obligatori aquí perquè hi ha un --delete pel mig: abans de la primera execució real d'un perfil nou sempre s'executa amb --simula. I el sufix .parcial amb mv final és l'escriptura atòmica de 08-03: mentre rsync treballa, el directori té un nom que ultima_correcta ignora; només el mv —atòmic dins del mateix sistema de fitxers— el converteix en còpia vàlida. Si el servidor s'apaga a mitges, queda brossa identificable, mai una còpia incompleta que sembli bona.
- Fase 2: manifest i verificació
Un directori amb fitxers no demostra res. El manifest és la prova:
escriu_manifest() {
local dir=$1 m="$1/MANIFEST"
{
printf '# copia %s de %s\norigen\t%s\n' "$NOM" "$(date -Is)" "$ORIGEN"
printf 'fitxers\t%s\n' "$(find "$dir" -type f ! -name MANIFEST | wc -l)"
printf 'bytes\t%s\n' "$(du -sb "$dir" | cut -f1)"
(cd "$dir" && find . -name "$VERIFICA_GLOB" -print0 | xargs -0 -r sha256sum)
} > "$m.parcial" && mv "$m.parcial" "$m"
}
verifica() {
local dir=$1 esperats reals
[[ -r $dir/MANIFEST ]] || { veloz_log_error "sense manifest: $dir"; return 1; }
esperats=$(awk -F'\t' '$1=="fitxers" {print $2}' "$dir/MANIFEST")
reals=$(find "$dir" -type f ! -name MANIFEST | wc -l)
(( esperats == reals )) || { veloz_log_error "recompte: $esperats vs $reals"; return 1; }
(cd "$dir" && grep -E '^[0-9a-f]{64} ' MANIFEST | sha256sum -c --quiet) || return 1
veloz_log_info "verificat: $dir ($reals fitxers)"
}Es verifiquen tres coses, de la més barata a la més cara: que el manifest existeixi, que el recompte coincideixi i que les sumes dels fitxers clau quadrin. Només es calculen sumes de VERIFICA_GLOB (els CSV) i no dels 40 GB de l'històric: verificar-ho tot cada nit costaria mitja hora i no aporta res, perquè els fitxers enllaçats amb --link-dest són les mateixes dades ja verificades ahir. El sha256sum -c --quiet retorna un codi diferent de zero si alguna cosa no quadra, i això n'hi ha prou per marcar el perfil com a fallit.
- Fase 3: retenció amb promoció
La retenció no és «esborrar el que és vell», és decidir quina còpia es converteix en setmanal i quina en mensual abans que caduqui com a diària.
aplica_retencio() {
local base=$1 d
# Promocio: la copia del dilluns passa a setmanals; la del dia 1, a mensuals.
for d in "$base/diaries"/*; do
[[ -d $d ]] || continue
local f=${d##*/}
[[ $(date -d "$f" +%u) == 1 ]] && enllaca_si_falta "$d" "$base/setmanals/$f"
[[ $(date -d "$f" +%d) == 01 ]] && enllaca_si_falta "$d" "$base/mensuals/$f"
done
poda "$base/diaries" "$RETENCIO_DIARIES"
poda "$base/setmanals" "$RETENCIO_SETMANALS"
poda "$base/mensuals" "$RETENCIO_MENSUALS"
}
poda() { # $1 = directori, $2 = quantes conservar
local dir=$1 conservar=$2 copies=() sobren
mapfile -t copies < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
(( ${#copies[@]} > conservar + 3 )) && { veloz_log_error "$dir: massa copies, revisa"; return 1; }
sobren=$(( ${#copies[@]} - conservar ))
for ((i = 0; i < sobren; i++)); do
veloz_log_info "esborrant ${copies[i]}"
[[ $SIMULACIO == si ]] || rm -rf "${dir:?dir buit}/${copies[i]:?copia buida}"
done
}La promoció fa servir enllaços durs (cp -al, dins d'enllaca_si_falta), no còpies: la setmanal del 3 d'agost comparteix dades amb la diària del mateix dia, així que promocionar costa zero bytes, i quan la diària s'esborri, la setmanal continuarà contenint les dades completes. És la mateixa propietat de --link-dest aplicada a la retenció. Hi ha a més dues salvaguardes de 08-03. La primera, ${dir:?} i ${copies[i]:?}: si per un error de configuració dir quedés buida, rm -rf "/${copies[i]}" esborraria des de l'arrel; amb :? l'script mor abans d'executar res. La segona, l'avís quan hi ha moltes més còpies de les esperades: significa que l'script fa dies que no poda i alguna cosa va malament; esborrar quinze directoris de cop sense avisar és justament el que no volem que faci tot sol.
- Fase 4: restauració i la seva prova automàtica
El subcomandament restaura mai escriu sobre l'original:
restaura() { # $1 = perfil, $2 = data o "ultima", $3 = desti
local base=$ARREL/$1 data=$2 desti=${3:-$(mktemp -d /tmp/restaura-XXXXXX)}
[[ $data == ultima ]] && data=$(ultima_correcta "$base/diaries")
local origen=$base/diaries/$data
[[ -d $origen ]] || veloz_morir 3 "no existeix la copia $1/$data"
verifica "$origen" || veloz_morir 4 "copia corrupta, no es restaura"
rsync -a "$origen/" "$desti/"
printf 'Restaurat %s (%s) a %s\n' "$1" "$data" "$desti"
}Es verifica abans de restaurar: restaurar una còpia corrupta sobre dades bones converteix un incident en un desastre. I el destí per defecte és un temporal, de manera que cal un acte deliberat per escriure a /srv. La prova de restauració automàtica és la peça que separa aquest projecte d'un simple script de còpia:
prova_restauracio() {
local tmp; tmp=$(mktemp -d); trap 'rm -rf "$tmp"' RETURN
restaura dades ultima "$tmp" >/dev/null || return 1
local orig=/srv/veloz/dades/enviaments.csv rest=$tmp/enviaments.csv
[[ -f $rest ]] || { veloz_log_error "prova: falta enviaments.csv"; return 1; }
cmp -s "$orig" "$rest" || { veloz_log_error "prova FALLIDA: el CSV difereix"; return 1; }
veloz_log_info "prova de restauracio OK ($(wc -l < "$rest") linies)"
}Corre cada diumenge amb el seu propi timer i compara el CSV recuperat amb l'original mitjançant cmp -s. Pot donar un fals negatiu si el fitxer va canviar entre la còpia i la prova; per això es compara contra el fitxer tal com estava —o s'accepta el fals positiu i s'investiga— però mai de la vida es desactiva la prova perquè deixi d'avisar. Una còpia de seguretat no verificada no existeix; una de verificada però mai restaurada, tampoc.
- Còpia remota i xifratge
sincronitza_remot() {
rsync -a --delete -e 'ssh -o BatchMode=yes' \
"$ARREL/" "copia@srv-veloz-03:/copies/$(hostname -s)/"
}
xifra_mensual() { # $1 = tar mensual
gpg --batch --yes --passphrase-file "$CLAU" -c --cipher-algo AES256 "$1" && shred -u "$1"
}BatchMode=yes (07-06) fa que SSH falli en lloc d'esperar una contrasenya que ningú no teclejarà a les tres de la matinada. Al remot es fa servir una clau restringida amb command= a authorized_keys, el mínim privilegi de 08-03: aquesta clau només pot rebre un rsync, no obrir una sessió. La clau de xifratge, per la seva banda, viu en un fitxer amb permisos 600 fora del repositori i fora de la mateixa còpia. Xifrar la còpia i guardar-hi la clau a dins és l'acudit més vell de l'ofici; i una clau que només és al servidor incendiat tampoc no serveix: la còpia de la frase de pas va al gestor de contrasenyes de l'empresa, i el runbook diu qui la pot recuperar.
- Espai en disc: comprovar abans de començar
Una còpia que omple el disc tomba el servei que pretenia protegir.
hi_ha_espai() { # $1 = origen, $2 = desti
local necessita disponible
necessita=$(du -sb --exclude=historic "$1" | cut -f1)
disponible=$(df -PB1 "$2" | awk 'NR==2 {print $4}')
(( disponible > necessita * 12 / 10 )) # 20 % de marge
}Es demana un 20 % de marge perquè --link-dest fa que el consum real sigui molt menor que la mida de l'origen, però el marge cobreix el dia que algú reescriu el CSV sencer. Si no hi cap: no s'executa el perfil, es registra un error, s'avisa i es continua amb el perfil següent. El que no es fa mai és esborrar còpies antigues per fer lloc —això converteix un problema d'espai en una pèrdua d'historial—.
- Observabilitat i codis de sortida
Cada perfil produeix el seu propi codi, i l'script agrega:
| Codi | Significat | Què fa l'operador |
|---|---|---|
| 0 | Tots els perfils bé i verificats | Res |
| 1 | Un perfil ha fallat, la resta bé | Revisar aquest perfil |
| 3 / 4 | Còpia no trobada / verificació fallida | Comprovar la data; si és corrupció, restaurar de srv-veloz-03 |
| 5 | Sense espai | Ampliar disc o revisar retenció |
El bucle de perfils no avorta a la primera fallada: acumula a l'array fallades i continua, perquè que el perfil de registres falli no és cap raó per quedar-se sense còpia de les dades. Al final, un resum (07-04):
resum() {
veloz_log_info "copia acabada: ${#perfils[@]} perfils, ${#fallades[@]} fallades, ${SECONDS}s"
(( ${#fallades[@]} == 0 )) || avisa_webhook "Copia amb fallades a $(hostname -s): ${fallades[*]}"
}SECONDS (04-06) dóna la durada sense restar marques de temps, i la seva tendència és una dada valuosa: si la còpia passa de tres a trenta minuts, alguna cosa ha canviat encara que el codi de sortida sigui 0.
- Posada en producció i runbook
Persistent=true (07-05) recupera l'execució si el servidor estava apagat a les 03:15, que és justament quan més falta fa la còpia. El RandomizedDelaySec evita que els tres servidors colpegin srv-veloz-03 al mateix segon. El servei executa copia.sh --tots amb flock (07-02) perquè dues execucions no se solapin mai.
El runbook (docs/runbook.md) conté l'única seqüència que importa a les quatre de la matinada, amb ordres copiables: comprovar l'última còpia correcta, verificar-la, restaurar a un temporal, comparar, i només llavors moure a producció. Un runbook que no s'ha executat mai és ficció: es prova al simulacre trimestral.
Errors Habituals i Consells
- Confondre còpia amb còpia de seguretat. Un
rsync --deletea un disc muntat al mateix servidor replica elrm -rfaccidental en segons. Cal retenció, i la retenció és la que converteix la còpia en còpia de seguretat. --link-destentre sistemes de fitxers diferents. No falla: fa còpies completes en silenci fins a omplir el disc. I sense.parcial+mv, un tall de llum deixa un directori incomplet que la retenció compta com a bo i que el dia de la restauració descobreixes buit.rm -rf "$dir/$copia"sense:?. El dia que una variable quedi buida, l'script esborra des de l'arrel. És la línia més perillosa de tot el toolkit.- Verificar la còpia amb la mateixa còpia. Desar el manifest només dins de la còpia detecta corrupció de dades, no un esborrat del conjunt: per això el recompte i la mida també es registren al log central.
- Consell: apunta al calendari una restauració real cada trimestre, amb cronòmetre. Si l'RTO mesurat supera l'RTO promès, el disseny està malament, no l'operador.
Exercicis
- Còpia de base de dades. Afegeix un perfil
postgresque faci servirpg_dump -Fca un fitxer abans de la còpia, amb la contrasenya a~/.pgpass(600) i verificació ambpg_restore -l. - Informe setmanal de l'estat de les còpies. Un subcomandament
informeque recorri tots els perfils i produeixi una taula amb: última còpia correcta, antiguitat en dies, mida, nombre de còpies per nivell i resultat de l'última verificació, en text i JSON. - Restauració parcial. Estén
restauraamb--fitxer CAMIper recuperar un sol fitxer, buscant en quines còpies existeix i en quina va canviar per última vegada.
Solucions
1. La clau és que el bolcat es fa abans del rsync i es verifica al moment:
pre_copia_postgres() {
local out=$ORIGEN/dump-$(date +%F).dump
pg_dump -Fc -f "$out.parcial" veloz || return 1
pg_restore -l "$out.parcial" >/dev/null || { rm -f "$out.parcial"; return 1; }
mv "$out.parcial" "$out"
find "$ORIGEN" -name 'dump-*.dump' -mtime +7 -delete
}pg_dump -Fc dóna una còpia consistent encara que la base estigui en ús; copiar-ne els fitxers en calent donaria una foto inconsistent (07-03). pg_restore -l llista el contingut sense restaurar: és la verificació més barata possible que el bolcat no està truncat. S'enganxa amb un PRE_ORDRE al .conf del perfil, invocat si està definit.
2. L'informe es recolza en els manifests ja escrits, així que no cal recalcular res:
informe() {
printf '%-12s %-12s %5s %8s %s\n' PERFIL ULTIMA DIES MIDA ESTAT
for base in "$ARREL"/*; do
local n=${base##*/} u; u=$(ultima_correcta "$base/diaries")
printf '%-12s %-12s %5d %8s %s\n' "$n" "${u:-cap}" \
"$(( ( $(date +%s) - $(date -d "$u" +%s) ) / 86400 ))" "$(du -sh "$base" | cut -f1)" \
"$(verifica "$base/diaries/$u" >/dev/null 2>&1 && echo OK || echo FALLADA)"
done
}La columna de dies és la que de debò es mira: una còpia «correcta» de fa nou dies és una alerta, encara que verifiqui bé.
3. Recórrer les còpies de més nova a més antiga amb find "$base" -path "*/$CAMI", i com que els fitxers no modificats comparteixen inode gràcies a --link-dest, comparar el número d'inode (stat -c %i) entre còpies consecutives identifica exactament en quina va canviar, sense llegir ni un sol byte del contingut.
Conclusió
copia.sh ja no és un script de còpia: és un sistema. Els perfils d'etc/copia.d/*.conf permeten afegir un conjunt sense tocar codi; rsync --link-dest dóna còpies que es generen com a incrementals i es restauren com a completes; el patró .parcial + mv garanteix que mai existeixi una còpia a mitges que sembli bona; el manifest amb recompte, mida i sumes converteix «hi ha fitxers» en «hi ha aquests fitxers i són aquests»; la retenció promociona amb enllaços durs abans de podar i esborra amb ${var:?} com a xarxa de seguretat; i la prova de restauració setmanal respon sola, cada diumenge, a l'única pregunta que importa: es pot recuperar? Al voltant, el que ja havies après: subshells per aïllar perfils (01-04), arrays per a opcions opcionals (04-03), mktemp i trap (05-02), escriptura atòmica i mínim privilegi (08-03), BatchMode a SSH (07-06), timer amb Persistent=true (07-05) i avís només quan hi ha fallades (07-04).
A 09-04 passem de protegir les dades a vigilar el servei. Construirem monitor-xarxa.sh: definir de manera mesurable què significa «la xarxa va bé», un catàleg d'objectius en fitxer, comprovacions que retornen 0/1/2 a l'estil dels connectors clàssics, execució en paral·lel perquè tot és espera de xarxa, reintents amb retrocés, alertes només en les transicions i un històric TSV del qual surt la disponibilitat percentual.
Curs de Programació en Bash
Mòdul 1: Introducció a Bash
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
