1,202 26/03/2026 07/04/2026 8 min

Quando un job cron copia file su S3, il problema non è quasi mai il comando in sé. Il problema è tutto intorno: rete instabile, credenziali esposte, log ingestibili, exit code ambigui e processi che restano appesi.

In hosting capita spesso con backup, export CSV, rotazioni di file e sync di cache. Qui vediamo un caso concreto: uno script bash che carica archivi compressi su S3 con rclone, retry controllati, lock per evitare esecuzioni doppie e gestione errori adatta a produzione.

Warning: un automatismo “semplice” che gira come root e usa chiavi in chiaro diventa un rischio operativo. Prima di automatizzare, va deciso dove sta il segreto, chi può leggere i log e come fallisce il job.

Prerequisiti

Lo scenario è questo:

  • server Linux con cron o systemd timer;
  • rclone configurato verso S3, Backblaze o storage compatibile;
  • cartella sorgente con archivi giornalieri;
  • utente dedicato, non root;
  • accesso a log di sistema o file di log dedicato.

Se usi un pannello come Plesk o cPanel, la parte di schedulazione si imposta anche da interfaccia.

In Plesk vai in Strumenti e impostazioniUtilità pianificate. In cPanel vai in Cron Jobs.

Note: la logica di retry, trap ed exit code resta comunque nello script. Il pannello avvia solo il comando.

Step 1: crea un utente dedicato e isola i segreti

Il primo errore da evitare è riusare l’utente amministratore. Un job di backup deve avere permessi minimi. Deve leggere solo ciò che serve e scrivere solo nella destinazione prevista.

Su shell, crea un utente tecnico e una directory privata per configurazione e log.

sudo useradd --system --home /var/lib/backup-sync --shell /usr/sbin/nologin backupsync
sudo install -d -o backupsync -g backupsync -m 0750 /var/lib/backup-sync
sudo install -d -o backupsync -g backupsync -m 0750 /var/log/backup-sync

Salva il file di configurazione di rclone nella home dell’utente, non in una directory condivisa.

sudo -u backupsync rclone config file

# Output: /var/lib/backup-sync/.config/rclone/rclone.conf

Proteggi il file con permessi stretti.

chmod 600 /var/lib/backup-sync/.config/rclone/rclone.conf

Warning: non passare access key e secret nella riga di comando. Finiscono nella history, nei log del processo o in strumenti di monitoraggio.

Step 2: scrivi uno script con trap, lock e retry

Qui serve una struttura da produzione, non un one-liner. Lo script deve:

  • bloccare esecuzioni concorrenti;
  • registrare timestamp e exit code;
  • interrompersi in caso di variabili non definite;
  • riprovare solo gli errori transitori;
  • uscire con codice chiaro per il monitoraggio.

Usa flock per impedire doppie esecuzioni. Usa trap per pulire file temporanei e loggare i fallimenti.

#!/usr/bin/env bash
set -Eeuo pipefail

LOCK_FILE="/run/backup-sync.lock"
LOG_FILE="/var/log/backup-sync/job.log"
SRC_DIR="/srv/exports/daily"
REMOTE="s3prod:archive-bucket/exports"
TMP_LIST="$(mktemp)"

cleanup() {
  rm -f "$TMP_LIST"
}

on_error() {
  local exit_code=$?
  echo "$(date -Is) ERROR exit=$exit_code line=${BASH_LINENO[0]} cmd=${BASH_COMMAND}" >> "$LOG_FILE"
  cleanup
  exit "$exit_code"
}

trap cleanup EXIT
trap on_error ERR

exec 9>"$LOCK_FILE"
flock -n 9 || { echo "$(date -Is) WARN already running" >> "$LOG_FILE"; exit 0; }

find "$SRC_DIR" -maxdepth 1 -type f -name '*.tar.gz' -print > "$TMP_LIST"

rclone copy --files-from "$TMP_LIST" \
  --retries 3 --low-level-retries 5 --retries-sleep 20s \
  --transfers 2 --checkers 4 \
  --log-file "$LOG_FILE" --log-level INFO \
  "$SRC_DIR" "$REMOTE"

# Output: upload completato con exit code 0 oppure errore tracciato nel log

La combinazione set -Eeuo pipefail è utile, ma non basta. Senza trap perdi il contesto dell’errore. Senza flock rischi due job in parallelo e un carico inutile sullo storage.

Note: il blocco sopra usa rclone direttamente. Se preferisci PowerShell su Windows Server, la logica è simile: try/catch, Start-Process -Wait, file lock e exit code non zero su errore.

Step 3: rendi il comportamento prevedibile con timeout e log separati

In produzione un job bloccato è peggio di un job fallito. Imposta un timeout esterno, così il sistema ammazza il processo se resta appeso su rete o DNS.

Con cron, l’approccio più semplice è richiamare timeout dal comando schedulato.

timeout --signal=TERM --kill-after=30s 25m /usr/local/bin/backup-sync.sh

# Output: il job termina entro 25 minuti o viene forzato con TERM e poi KILL

Separare i log aiuta molto. Tieni un file per il log applicativo e un altro per l’orchestrazione. Il primo lo scrive rclone. Il secondo lo scrive lo script.

Un esempio di wrapper minimo in cron:

*/30 * * * * backupsync timeout --signal=TERM --kill-after=30s 25m /usr/local/bin/backup-sync.sh

Se usi un pannello, inserisci il comando completo nella riga cron. In Plesk e cPanel non serve inventare altro. Il punto è mantenere identico il comportamento in produzione e in test.

Perché non basta “|| exit 1”

Un semplice fallback cattura solo il fallimento finale. Non registra linee, comandi e punto di rottura. In un ambiente hosting, questo fa perdere tempo durante incidenti notturni.

Con trap ERR sai dove è caduto lo script. Con flock eviti la causa più banale ma frequente: il job precedente è ancora vivo.

Step 4: limita il danno con permessi, umask e variabili pulite

Molti script funzionano, ma lasciano superfici inutili. La parte di sicurezza non è decorativa. Serve a evitare che un altro utente legga file temporanei o che il job erediti ambiente sporco.

Aggiungi umask e una whitelist minima delle variabili.

umask 077
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
unset CDPATH

# Output: file creati con permessi privati e ambiente ridotto

Se il job deve leggere dati sensibili, evita directory world-readable. Se deve scrivere file temporanei, usa /run o /tmp con file unici e permessi stretti.

Warning: non salvare token S3 in uno script dentro /usr/local/bin con permessi 755. È un errore classico e ancora troppo comune.

Step 5: aggiungi un controllo preflight prima del trasferimento

Prima di copiare tutto, verifica spazio, connettività e presenza della configurazione. Così fallisci prima e in modo leggibile.

[ -r /var/lib/backup-sync/.config/rclone/rclone.conf ] || exit 10
[ -d "$SRC_DIR" ] || exit 11
command -v rclone >/dev/null || exit 12

# Output: il job si ferma subito con codice specifico se manca un prerequisito

Questo aiuta anche il monitoraggio. Un codice 10 per config mancante è molto più utile di un generico 1.

Se vuoi un controllo più forte, aggiungi una verifica di connettività verso l’endpoint S3, ma senza esporre chiavi nei log.

rclone lsd s3prod:archive-bucket >/dev/null

# Output: elencazione bucket senza errori di autenticazione o rete

Step 6: integra il job con monitoraggio e alert

Un job da produzione non deve solo girare. Deve anche parlare quando fallisce. La soluzione più semplice è un controllo sul codice di uscita.

Se usi systemd, puoi far partire lo script da service e timer. Il vantaggio è il logging unificato e i restart controllati. Se usi cron, aggiungi un alert esterno o un check su file di stato.

Un approccio pratico è scrivere l’ultima esecuzione e il risultato in un file dedicato.

STATUS_FILE="/var/lib/backup-sync/last-run.status"

echo "$(date -Is) OK" > "$STATUS_FILE"

# Output: file di stato aggiornato con timestamp e OK

Per il monitoraggio, puoi collegare un alert su mtime del file oppure su assenza di nuove righe nel log. In ambienti più maturi, esporta il check verso Prometheus o un sistema di synthetic monitoring.

Verifica finale

Prima di mettere lo script in produzione, fai tre test separati.

  • Test 1: esegui lo script manualmente con un file piccolo.
  • Test 2: blocca temporaneamente la rete o usa un endpoint sbagliato per verificare il retry.
  • Test 3: lancia due istanze insieme e conferma che flock ne lascia partire una sola.

Controlla questi punti:

  • il log mostra timestamp e codice d’uscita;
  • il lock file impedisce doppie esecuzioni;
  • le credenziali non compaiono in ps, history o log;
  • il timeout interrompe processi appesi;
  • il job termina con exit code coerente.

Esempio di controllo rapido:

tail -n 20 /var/log/backup-sync/job.log
echo $?

# Output: ultime righe del job e codice di uscita 0 oppure un errore specifico

Troubleshooting

1) flock: cannot open lock file /run/backup-sync.lock: No such file or directory

Causa: la directory /run non contiene il file o il servizio parte prima della creazione della cartella.

Fix:

sudo install -d -o backupsync -g backupsync -m 0750 /run
sudo touch /run/backup-sync.lock
sudo chown backupsync:backupsync /run/backup-sync.lock

# Output: il lock file diventa scrivibile dall’utente tecnico

2) rclone: failed to create file system for "s3prod:archive-bucket": no such remote

Causa: il remote non esiste nella configurazione dell’utente che esegue il job.

Fix:

sudo -u backupsync rclone config
sudo -u backupsync rclone listremotes

# Output: il remote s3prod compare nell’elenco dei remoti disponibili

3) ERROR : Failed to copy: context canceled

Causa: il comando è stato interrotto da timeout esterno o da una rete instabile durante il trasferimento.

Fix:

timeout --signal=TERM --kill-after=30s 40m /usr/local/bin/backup-sync.sh

# Output: più tempo disponibile al job, con stop controllato se resta bloccato

Conclusione

Un job ripetitivo funziona davvero solo se fallisce bene. Lock, trap, timeout, permessi stretti e log leggibili fanno la differenza tra automazione e roulette.

Il prossimo passo concreto è trasformare questo script in servizio systemd con timer, oppure in cron con alert esterno sul codice di uscita. Da lì puoi aggiungere retention, checksum e test di restore periodici.