Comment exécuter une commande sur plusieurs serveurs en SSH

Mettez les noms des serveurs dans un fichier et parcourez-le avec ssh -n -o BatchMode=yes "$host" 'command'. Les serveurs passent l'un après l'autre et aucun ne bloque la boucle. Pour aller plus vite, lancez-les en parallèle avec xargs -P, parallel --tag de GNU ou parallel-ssh -h hosts -i, qui indiquent le serveur de chaque sortie. Pour tout ce qui modifie un serveur, essayez d'abord sur un seul et vérifiez le code de sortie de chacun.

Mis à jour le . Toutes les commandes ont été lancées depuis un conteneur Debian 13 vers trois conteneurs Debian 13 avec OpenSSH 10.0, avec bash 5.2, GNU parallel 20240222, parallel-ssh 2.3.5, tmux 3.5a et ansible-core 2.19.

Avant de commencer : une liste d'hôtes et des connexions sans question

Toutes les méthodes ci-dessous lisent la même chose : un nom de serveur par ligne. Utilisez les noms de votre configuration SSH, pour que utilisateurs, ports et clés viennent de là et pas de chaque commande. Voir les exemples de fichier ssh config si vous n'en avez pas encore.

web1
web2
web3

Enregistrez-le sous hosts. Deux options rendent les lancements en série plus sages :

  • -o BatchMode=yes échoue tout de suite au lieu d'attendre une demande de mot de passe ou de phrase de passe à laquelle personne ne répondra.
  • -o ConnectTimeout=5 empêche un serveur éteint de bloquer les autres.

Chargez d'abord votre clé dans l'agent avec ssh-add, pour qu'aucun lancement ne s'arrête pour la demander. Pour de nombreux lancements à la suite, le multiplexage SSH avec ControlMaster réutilise une connexion par serveur et fait démarrer chaque commande plus vite.

Un serveur après l'autre : une boucle

La boucle est la méthode la plus simple, et la sortie reste dans l'ordre. Celle-ci garde aussi la liste des serveurs où la commande a échoué :

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: 

Le -n compte. Sans lui, ssh lit le reste de hosts comme sa propre entrée, et la boucle s'arrête après le premier serveur. Les règles de guillemets et les codes de sortie sont les mêmes que pour un seul serveur, voir comment exécuter une commande en SSH.

Avec beaucoup de serveurs, la boucle est lente : cinquante serveurs à deux secondes chacun prennent presque deux minutes.

En parallèle : tâches de fond, xargs et GNU parallel

Bash seul peut lancer chaque ssh en arrière-plan et attendre qu'ils finissent tous. Chaque serveur écrit dans son propre fichier, donc les sorties ne se mélangent pas :

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

Tout part en même temps. Pour limiter le nombre de lancements simultanés, utilisez xargs -P :

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

Les lignes arrivent dans l'ordre où les serveurs terminent, alors faites afficher le nom d'hôte par la commande.

GNU parallel fait les deux mieux : --tag met le nom du serveur devant chaque ligne, et --joblog écrit le code de sortie de chaque serveur dans un fichier :

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

Ici, web3 était éteint. parallel sort avec le nombre de tâches en échec, 1 dans ce cas, donc un script peut s'arrêter dessus.

parallel-ssh (pssh)

parallel-ssh est fait pour ce travail. Sous Debian et Ubuntu, c'est apt install pssh, et la commande s'appelle parallel-ssh. Sur d'autres systèmes, elle s'appelle 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

La commande vérifie qu'un dossier existe : il manque sur web1, et web3 est éteint. -i affiche la sortie de chaque serveur sous sa ligne d'état, et -t 10 abandonne un serveur après 10 secondes. Pour garder la sortie, écrivez plutôt un fichier par serveur :

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

Le code de sortie n'est pas nul dès qu'un serveur a échoué. Ajoutez -p pour limiter le nombre de lancements simultanés.

Taper dans plusieurs terminaux à la fois : tmux

Parfois, il faut suivre chaque serveur, ou répondre à une question sur chacun. tmux peut ouvrir un panneau par serveur et envoyer chaque touche à tous :

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

Désactivez-le depuis tmux avec Ctrl-b : puis setw synchronize-panes off. Tant qu'il est actif, tout part partout, faute de frappe comprise. L'aide-mémoire tmux donne les touches pour passer d'un panneau à l'autre.

Avant une commande qui modifie quelque chose

Une erreur sur cinquante serveurs coûte cinquante fois plus. Quelques habitudes la gardent petite :

  • Prévisualiser d'abord. Beaucoup d'outils ont un mode d'essai. apt-get -s upgrade | grep -c ^Inst compte les paquets qui changeraient, sans rien changer.
  • Un serveur, puis les autres. Lancez la modification sur le premier serveur, vérifiez, puis sur les autres.
  • Demander avant le lancement. Dans un script, affichez le nombre de serveurs et attendez une réponse.
  • Lire chaque code de sortie. Un lancement réussi sur 49 serveurs et raté sur un seul est un échec tant que vous ne savez pas pourquoi.
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

Gardez la production dans un fichier d'hôtes à part, pour qu'une commande prévue pour la préproduction ne l'atteigne jamais par accident. D'autres habitudes sont dans comment éviter de lancer des commandes sur le mauvais serveur.

Quand Ansible convient mieux

Quand la même modification revient chaque semaine, un outil qui décrit le résultat vaut mieux qu'une commande shell. Le mode ad hoc d'Ansible lit la même liste :

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

Il indique rc pour chaque serveur, et des modules comme apt ou copy peuvent montrer ce qu'ils changeraient avec --check. Pour un coup d'œil rapide à quelques serveurs, la boucle ou parallel-ssh demandent moins de travail. Voir comment gérer de nombreux serveurs SSH pour tenir la liste elle-même en ordre.

Questions

Pourquoi ma boucle ssh s'arrête-t-elle après le premier serveur ?

ssh lit l'entrée standard et emporte le reste de la liste des hôtes. Ajoutez -n à la commande ssh, ou utilisez une boucle for qui ne lit pas l'entrée standard.

Sur combien de serveurs puis-je lancer en même temps ?

Autant que votre machine et votre réseau le permettent. De dix à cinquante à la fois, c'est courant. Baissez ce nombre si la commande est lourde ou si les serveurs partagent une ressource comme une base de données.

Quelle différence entre parallel-ssh et Ansible ?

parallel-ssh lance une commande shell sur de nombreux serveurs. Ansible lance des modules qui décrivent le résultat voulu, peut vérifier avant de modifier, et garde inventaires et playbooks pour le travail répété.

Puis-je taper dans plusieurs sessions SSH à la fois ?

Oui. Dans tmux, mettez chaque session dans un panneau et activez synchronize-panes. Chaque touche va à tous les panneaux jusqu'à ce que vous le désactiviez.

Sources

Autres guides : Sessions et tmux

Tous les guides