Cómo ejecutar un comando en varios servidores por SSH

Pon los nombres de los servidores en un archivo y recórrelo con ssh -n -o BatchMode=yes "$host" 'command'. Así se ejecuta en un servidor tras otro y no se queda parado en ninguno. Para ir más rápido, ejecútalo en paralelo con xargs -P, GNU parallel --tag o parallel-ssh -h hosts -i, que marcan cada salida con su servidor. Si el comando cambia algo, pruébalo antes en uno y revisa el código de salida de cada servidor.

Actualizada el . Todos los comandos se ejecutaron desde un contenedor Debian 13 contra tres contenedores Debian 13 con OpenSSH 10.0, con bash 5.2, GNU parallel 20240222, parallel-ssh 2.3.5, tmux 3.5a y ansible-core 2.19.

Antes de empezar: una lista de hosts y conexiones sin preguntas

Todos los métodos de abajo leen lo mismo: un nombre de servidor por línea. Usa los nombres de tu configuración SSH, así el usuario, el puerto y la clave salen de ahí y no de cada comando. Consulta los ejemplos del archivo ssh config si aún no tienes uno.

web1
web2
web3

Guárdalo como hosts. Dos opciones hacen que las ejecuciones en lote se comporten bien:

  • -o BatchMode=yes falla al instante en lugar de esperar en una petición de contraseña o frase de paso que nadie va a responder.
  • -o ConnectTimeout=5 evita que un servidor caído retenga a los demás.

Carga antes tu clave en el agente con ssh-add, para que ninguna ejecución se pare a pedirla. Para muchas ejecuciones seguidas, la multiplexación SSH con ControlMaster reutiliza una conexión por servidor y hace que cada comando arranque antes.

Un servidor tras otro: un bucle

Un bucle es lo más sencillo, y la salida sale en orden. Este además guarda una lista de los servidores donde el comando falló:

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: 

El -n es importante. Sin él, ssh lee el resto de hosts como su propia entrada y el bucle termina tras el primer servidor. Las reglas de comillas y los códigos de salida son los mismos que con un solo servidor, consulta cómo ejecutar un comando por SSH.

Con muchos servidores un bucle es lento: cincuenta servidores a dos segundos cada uno tardan casi dos minutos.

En paralelo: trabajos en segundo plano, xargs y GNU parallel

Con bash solo puedes lanzar cada ssh en segundo plano y esperar a todos. Cada servidor escribe en su propio archivo, así las salidas no se mezclan:

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

Así arrancan todos a la vez. Para limitar cuántos se ejecutan juntos, usa xargs -P:

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

Las líneas llegan en el orden en que terminan los servidores, así que haz que el comando imprima su propio nombre de host.

GNU parallel hace las dos cosas mejor: --tag pone el nombre del servidor delante de cada línea y --joblog escribe en un archivo el código de salida de cada servidor:

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

Aquí web3 estaba caído. parallel sale con el número de trabajos que fallaron, 1 en este caso, así que un script puede detenerse con eso.

parallel-ssh (pssh)

parallel-ssh está hecho para esto. En Debian y Ubuntu se instala con apt install pssh y el comando se llama parallel-ssh. En otros sistemas el comando se llama 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

El comando comprueba que existe una carpeta: falta en web1 y web3 está caído. -i muestra la salida de cada servidor bajo su línea de estado, y -t 10 abandona un servidor a los 10 segundos. Para guardar la salida, escribe un archivo por servidor:

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

Sale con un código distinto de cero si algún servidor falló. Añade -p para limitar cuántos se ejecutan a la vez.

Escribir en muchas terminales a la vez: tmux

A veces necesitas ver cada servidor o responder a una pregunta en cada uno. tmux puede abrir un panel por servidor y enviar cada tecla que pulsas a todos:

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

Desactívalo desde tmux con Ctrl-b : y setw synchronize-panes off. Mientras está activo, todo va a todas partes, también una errata. La chuleta de tmux recoge las teclas para moverte entre paneles.

Antes de un comando que cambia cosas

Un error en cincuenta servidores cuesta cincuenta veces más. Unos pocos hábitos lo mantienen pequeño:

  • Primero una vista previa. Muchas herramientas tienen modo de prueba. apt-get -s upgrade | grep -c ^Inst cuenta los paquetes que cambiarían, sin cambiarlos.
  • Un servidor, luego el resto. Aplica el cambio en el primer servidor, compruébalo y después en los demás.
  • Pregunta antes de ejecutar. En un script, muestra el número de servidores y espera una respuesta.
  • Lee cada código de salida. Una ejecución que funcionó en 49 servidores y falló en uno es una ejecución fallida hasta que sepas por qué.
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

Guarda producción en un archivo de hosts aparte, para que un comando pensado para staging nunca llegue a ella por accidente. Tienes más hábitos en cómo evitar ejecutar comandos en el servidor equivocado.

Cuándo encaja mejor Ansible

Cuando el mismo cambio vuelve cada semana, una herramienta que describe el resultado es mejor que un comando de shell. El modo ad hoc de Ansible lee la misma lista:

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

Indica el rc de cada servidor, y módulos como apt o copy pueden mostrar lo que cambiarían con --check. Para echar un vistazo rápido a unos pocos servidores, el bucle o parallel-ssh dan menos trabajo. Consulta cómo gestionar muchos servidores SSH para mantener ordenada la propia lista.

Preguntas

¿Por qué mi bucle de ssh se detiene después del primer servidor?

ssh lee la entrada estándar y se lleva el resto de la lista de hosts. Añade -n al comando ssh, o usa un bucle for que no lea de la entrada estándar.

¿En cuántos servidores puedo ejecutar a la vez?

En tantos como permitan tu equipo y tu red. Entre diez y cincuenta a la vez es habitual. Baja el número si el comando es pesado o si los servidores comparten un recurso, como una base de datos.

¿Qué diferencia hay entre parallel-ssh y Ansible?

parallel-ssh ejecuta un comando de shell en muchos servidores. Ansible ejecuta módulos que describen el resultado que quieres, puede comprobar antes de cambiar y guarda inventarios y playbooks para el trabajo que se repite.

¿Puedo escribir en varias sesiones SSH a la vez?

Sí. En tmux, pon cada sesión en un panel y activa synchronize-panes. Cada tecla va a todos los paneles hasta que lo desactives.

Fuentes

Más en Sesiones y tmux

Todas las guías