Come eseguire un comando su più server via SSH

Metti i nomi dei server in un file e scorrilo con ssh -n -o BatchMode=yes "$host" 'comando'. Così i server vengono eseguiti uno dopo l'altro e nessuno blocca il ciclo. Per fare prima, eseguili in parallelo con xargs -P, GNU parallel --tag o parallel-ssh -h hosts -i, che indicano il server accanto a ogni output. Per un comando che modifica i server, provalo prima su uno e controlla il codice di uscita di ciascuno.

Aggiornata il . Tutti i comandi sono stati eseguiti da un container Debian 13 verso tre container Debian 13 con OpenSSH 10.0, con bash 5.2, GNU parallel 20240222, parallel-ssh 2.3.5, tmux 3.5a e ansible-core 2.19.

Prima di cominciare: un elenco di host e accessi senza domande

Tutti i metodi qui sotto leggono la stessa cosa: un nome di server per riga. Usa i nomi della configurazione SSH, così utenti, porte e chiavi arrivano da lì e non da ogni comando. Se non ce l'hai ancora, vedi gli esempi di file ssh config.

web1
web2
web3

Salvalo come hosts. Due opzioni rendono ordinate le esecuzioni in serie:

  • -o BatchMode=yes fallisce subito invece di aspettare una password o una passphrase a cui nessuno risponderà.
  • -o ConnectTimeout=5 evita che un server spento blocchi gli altri.

Prima carica la chiave nell'agente con ssh-add, così nessuna esecuzione si ferma a chiederla. Per molte esecuzioni di fila, il multiplexing SSH con ControlMaster riusa una connessione per server e fa partire ogni comando più in fretta.

Un server dopo l'altro: un ciclo

Un ciclo è il modo più semplice, e l'output resta in ordine. Questo tiene anche l'elenco dei server dove il comando è fallito:

failed=()
while read -r host; do
  printf '%s: ' "$host"
  ssh -n -o BatchMode=yes -o ConnectTimeout=5 "$host" 'uptime -p' || failed+=("$host")
done < hosts
echo "failed: ${failed[*]}"
web1: up 4 days, 9 hours, 24 minutes
web2: up 4 days, 9 hours, 24 minutes
web3: up 4 days, 9 hours, 24 minutes
failed: 

Il -n è importante. Senza, ssh legge il resto di hosts come proprio input e il ciclo finisce dopo il primo server. Virgolette e codici di uscita funzionano come con un solo server: vedi come eseguire un comando via SSH.

Con molti server un ciclo è lento: cinquanta server da due secondi l'uno richiedono quasi due minuti.

In parallelo: lavori in background, xargs e GNU parallel

Con bash puoi avviare ogni ssh in background e aspettarli tutti. Ogni server scrive nel proprio file, così gli output non si mescolano:

mkdir -p out
while read -r host; do
  ssh -n -o BatchMode=yes "$host" 'hostname; uname -r' > "out/$host.log" 2>&1 &
done < hosts
wait
head out/*.log

Così partono tutti insieme. Per limitare quanti ne girano contemporaneamente, usa xargs -P:

xargs -P 10 -I{} ssh -n -o BatchMode=yes {} 'echo "$(hostname): $(nproc) cpu"' < hosts

Le righe arrivano nell'ordine in cui i server finiscono, quindi fai stampare al comando il nome del proprio host.

GNU parallel fa meglio entrambe le cose: --tag mette il nome del server davanti a ogni riga e --joblog scrive in un file il codice di uscita di ciascun server:

parallel -j 10 --tag --joblog run.log ssh -n -o BatchMode=yes -o ConnectTimeout=5 {} hostname :::: hosts
cut -f 7,9 run.log
Exitval	Command
255	ssh -n -o BatchMode=yes -o ConnectTimeout=5 web3 hostname
0	ssh -n -o BatchMode=yes -o ConnectTimeout=5 web2 hostname
0	ssh -n -o BatchMode=yes -o ConnectTimeout=5 web1 hostname

Qui web3 era spento. parallel esce con il numero di lavori falliti, 1 in questo caso, così uno script può fermarsi.

parallel-ssh (pssh)

parallel-ssh è fatto proprio per questo. Su Debian e Ubuntu si installa con apt install pssh e il comando si chiama parallel-ssh. Su altri sistemi il comando si chiama pssh.

parallel-ssh -h hosts -i -t 10 'test -d /srv/app'
[1] 15:20:02 [FAILURE] web3 Exited with error code 255
Stderr: ssh: Could not resolve hostname web3: Name or service not known
[2] 15:20:02 [FAILURE] web1 Exited with error code 1
[3] 15:20:02 [SUCCESS] web2

Il comando controlla che una cartella esista: manca su web1 e web3 è spento. -i stampa l'output di ogni server sotto la sua riga di stato, e -t 10 rinuncia a un server dopo 10 secondi. Per conservare l'output, scrivi invece un file per server:

parallel-ssh -h hosts -t 10 -o out -e err 'df -h /'

Esce con un codice diverso da zero se un server qualsiasi è fallito. Aggiungi -p per limitare quanti ne girano insieme.

Scrivere in molti terminali insieme: tmux

A volte devi guardare ogni server, o rispondere a una domanda su ognuno. tmux può aprire un riquadro per server e mandare a tutti ogni tasto che premi:

tmux new-session -d -s multi 'ssh web1'
tmux split-window -t multi 'ssh web2'
tmux split-window -t multi 'ssh web3'
tmux select-layout -t multi tiled
tmux set-window-option -t multi synchronize-panes on
tmux attach -t multi

Per disattivarlo da dentro tmux usa Ctrl-b : e poi setw synchronize-panes off. Finché è attivo, tutto va dappertutto, errori di battitura compresi. Il promemoria di tmux elenca i tasti per spostarsi fra i riquadri.

Prima di un comando che modifica qualcosa

Un errore su cinquanta server costa cinquanta volte tanto. Poche abitudini lo tengono piccolo:

  • Prima un'anteprima. Molti strumenti hanno una prova a vuoto. apt-get -s upgrade | grep -c ^Inst conta i pacchetti che cambierebbero, senza cambiarli.
  • Un server, poi gli altri. Esegui la modifica sul primo server, controlla, poi passa agli altri.
  • Chiedi prima di partire. In uno script, mostra quanti server sono e aspetta una risposta.
  • Leggi ogni codice di uscita. Un'esecuzione riuscita su 49 server e fallita su uno è fallita, finché non sai perché.
first=$(head -n 1 hosts)
ssh -n "$first" 'apt-get -s upgrade | grep -c ^Inst'
n=$(wc -l < hosts)
read -r -p "Run on $n servers? [y/N] " ok
[ "$ok" = y ] || exit 1

Tieni la produzione in un file di host separato, così un comando pensato per lo staging non ci arriva per sbaglio. Altre abitudini sono in come evitare di eseguire comandi sul server sbagliato.

Quando conviene Ansible

Quando la stessa modifica torna ogni settimana, uno strumento che descrive il risultato è meglio di un comando di shell. La modalità ad hoc di Ansible legge lo stesso elenco:

ansible all -i 'web1,web2,web3,' -m ansible.builtin.command -a 'uptime -p' --forks 10
web1 | CHANGED | rc=0 >>
up 4 days, 9 hours, 25 minutes

Riporta rc per ogni server, e moduli come apt o copy mostrano cosa cambierebbero con --check. Per un'occhiata veloce a pochi server, il ciclo o parallel-ssh richiedono meno lavoro. Per tenere in ordine l'elenco stesso, vedi come gestire molti server SSH.

Domande

Perché il mio ciclo ssh si ferma dopo il primo server?

ssh legge lo standard input e si porta via il resto dell'elenco dei server. Aggiungi -n al comando ssh, oppure usa un ciclo for che non legge dallo standard input.

Su quanti server posso eseguire un comando contemporaneamente?

Su quanti ne reggono il tuo computer e la rete. Da dieci a cinquanta insieme è normale. Abbassa il numero se il comando è pesante o se i server condividono una risorsa come un database.

Che differenza c'è fra parallel-ssh e Ansible?

parallel-ssh esegue un comando di shell su molti server. Ansible esegue moduli che descrivono il risultato voluto, può controllare prima di modificare e conserva inventari e playbook per il lavoro che si ripete.

Posso scrivere in più sessioni SSH insieme?

Sì. In tmux metti ogni sessione in un riquadro e attiva synchronize-panes. Ogni tasto va a tutti i riquadri finché non lo disattivi.

Fonti

Altro in Sessioni e tmux

Tutte le guide