Run a shell command on multiple servers over ssh simultaneously

If you have multiple Linux servers out there and have been managing them by hand, there's a better way. You have probably heard of Chef, Puppet, and Ansible, and might have heard that Chef and Puppet are a bit troublesome to get up and running. But there's a simpler way to just connect to a few of your Linux (or BSD) servers and run commands on them.

Example: Tell me what OS and version is running on all my servers

Step 1. Set up your ansible hosts file.

Quick version: Create the file named 'hosts' and put servername ansible_ssh_host=hostname in the file for each of your servers, replacing servername with a name and hostname with the real DNS hostname of the server, or IP address.

Long version:

So first you need ansible to be installed so go install that on your local desktop if you haven't already. Once you have the ansible client, you can immediately connect to your servers without installing or configuring anything on them (assuming they have ssh and python).

For ansible to connect to anything, it needs a 'hosts' file, also called an inventory file, even if it only has one host. The ansible commands ansible and ansible-playbook lack the option to pass in a single unconfigured host as an argument, so you really do need a hosts file.

Ansible's hosts file differs in format from '/etc/ hosts' (removing space breaks Cloudflare) but is simple enough to create by hand. Here's a sample file with just 1 line generated by Vagrant:

default ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222

Following the example, replace 'default' with a hostname or nickname for a server, then replace '127.0.0.1' with the actual IP address or hostname of the server, and remove ansible_ssh_port=2222 or change 2222 to 22 or the actual port the server's sshd is listening at. Add all your servers.

If you have so many servers that you want to organize them into logical groups you can use the brackets syntax for groups. Put a group naem in brackets e.g. [webservers] and list the servers on the subsequent lines followed by a blank line to separate groups.

[webservers]
host1 ansible_ssh_host=10.0.0.1
host2 ansible_ssh_host=10.0.0.2
myserver ansible_ssh_host=www.myserver.com

Save this to a file called 'hosts'.

Step 2. Run your command ad hoc through ansible

Setting up hosts takes some effort up front, but then you can use the hosts file every time. The next step depends on if you want to use an ansible module command with their special syntax or just run a raw shell command. ("Ad hoc" in ansible speak means "without a playbook", so we use the ansible command instead of ansible-playbook

Let's test our hosts file with some easy commands.

$ ansible all -i ./hosts -m ping

Be in the directory where you created 'hosts' or use the full or relative path to the file in the -i argument. This will 'ping' all your hosts. This is an ansible "ping" using the ansible module called "ping", it tests that ansible can successfully connect to each host. It is not the same as running ping host from your machine.

$ ansible all -i ./hosts -m command -a 'ls -la'
$ ansible all -i ./hosts -a 'ls -la'

This will login to each host and run ls -la and show you the output. You can replace ls with any other command! The two lines above will do the same thing because command is the default value for -m.

$ ansible all -i ./hosts -m shell -a 'ls -la'

This does the same thing as the previous command but notice we replaced the 'command' module with the 'shell' module. The difference is if we tried to use characters like &, |, > or >> to pipe command output to files or other processes. This won't work with -m command because the command ('ls') will receive each of those strings as an argument. In the case of -m shell, the string is interpreted through /bin/bash first, which will parse the command line.

$ ansible all -i ./hosts -m shell -a 'ls -la | grep taint > tainted_files'
$ ansible all -i ./hosts -m fetch -a 'src=tainted_files dest=taint.{{ ansible_hostname }}'

The first command demonstrates something we can do with -m shell but not -m command. The second line shows how we can copy files from multiple servers to our local machine with one command (the actual files will be stored in subdirectories, but you should be able to find them and the important thing is that they are saved separately). In that example, ansible_hostname is a variable which was defined in our hosts file above.

It's more common to want to copy a file to multiple servers and you use -m copy to scp files to them.