Ansible Tutorial
Introduction
In this short Ansible tutorial we will go through the installation process of Ansible and its basic features. Ansible is an automation tool which allows us to manage remote servers without installing any agents on those remote machines. Only the control machine needs to have it installed. By “remote servers” I mean not only Linux/Windows servers, but any equipment that can be managed via SSH, for example network equipment. Ansible uses SSH protocol to connect to remote machines and perform required actions. Besides that, note that remote machines should have python installed, but this should not be a problem, as almost any Linux distribution comes with python by default. I will use Ubuntu 18.04 for demonstration, but Ansible can be installed on any Linux machine.
Installing Ansible
The following command will install Ansible on your control machine:
orkhans@matrix:~$ sudo apt-get install ansible
You can verify that you have successfully installed it:
administrator@ubu3:~$ ansible --version ansible 2.5.1 config file = /etc/ansible/ansible.cfg configured module search path = [u'/home/administrator/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python2.7/dist-packages/ansible executable location = /usr/bin/ansible python version = 2.7.15rc1 (default, Apr 15 2018, 21:51:34) [GCC 7.3.0]
Configure SSH
After we have Ansible installed we need to make sure that it can log in to remote machines via SSH. We will generate an SSH key pair and copy our public key to the remote machine, so that Ansible can login to them automatically without prompting us for passwords.
Go to your control machine and run the following command to generate a key pair:
orkhans@matrix:~$ ssh-keygen
Then copy the public key to each of the remote machines which should be managed by Ansible:
orkhans@matrix:~$ ssh-copy-id 192.168.37.76 /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys administrator@192.168.37.76's password: Number of key(s) added: 1 Now try logging into the machine, with: "ssh '192.168.37.76'" and check to make sure that only the key(s) you wanted were added.
In my case IP address of the first managed machine is 192.168.37.76, therefore I issued ssh-copy-id 192.168.37.76 on my control machine.
Now I can login to 192.168.37.76 using the following command ssh 192.168.37.76 without specifying the password.
I also run the same command for the second managed machine (192.168.37.161):
orkhans@matrix:~$ ssh-copy-id 192.168.37.161 /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys administrator@192.168.37.161's password: Number of key(s) added: 1 Now try logging into the machine, with: "ssh '192.168.37.161'" and check to make sure that only the key(s) you wanted were added.
Edit /etc/ansible/hosts file
Now we need to tell our control server which remote machines it is going to manage,therefore we should specify either IP address or a hostname of each managed machine in Ansible configuration file.
Add managed machines’ IP addresses (in my case 192.168.37.76 and 192.168.37.161) to /etc/ansible/hosts file on the Control machine, just go to the bottom of the file and append IP addresses. Don’t pay attention to the lines starting with #, they are just comments and have no effect.
orkhans@matrix:~$ cat /etc/ansible/hosts # This is the default ansible 'hosts' file. # # It should live in /etc/ansible/hosts 192.168.37.76 192.168.37.161
This is the summary of what we have done so far:
- Installed Ansible on a Control Machine
- Generated SSH key pair and copied public key to the managed machine
- Edited /etc/ansible/hosts file on a Control Machine and added managed machines’ IP addresses. This makes Ansible aware of managed machines.
Ansible Modules
Now that we have our SSH keys exchanged and our Control Server is aware of managed machines we can start actually managing them, that is run any shell commands, install/delete software, enable/disable any services, copy files etc.
Ansible uses modules for doing all these things , and there’s a separate module for each task, for example, we will use shell module to run a shell command on a remote machine, and we will use service module to manage services. This modular architecture makes Ansible extremely flexible and enables us to create new modules should the need arise. For example, when a new switch/router with a completely new software comes out, you can create your own module to manage it.
We can run these commands either as one-line commands (ad-hoc commands) or using scripts (playbooks). In both cases we are going to use the same modules, playbooks just allow us to build more complicated tasks.
Ad-hoc commands
Let’s run some ad-hoc commands and see how different modules are used.
ping module
Ping allows us to check connectivity and make sure that control server can reach managed machines:
orkhans@matrix:~$ ansible all -m ping 192.168.37.76 | SUCCESS => { "changed": false, "ping": "pong" } 192.168.37.161 | SUCCESS => { "changed": false, "ping": "pong" }
Let’s have a look at the command syntax:
all – tells Ansible to run this task for all managed machines, specified in our hosts file. We might also divide them in groups and run this command for a specific group.
-m ping – “-m” stands for “module” and is followed by the name of the module. In this case we want to use ping module.
You can see that we get a response from each machine. “SUCCESS” tells us that everything went smoothly.
shell module
One of the most useful modules is shell, because it allows to run any shell command on a remote machine. We will run uptime command on our managed machines:
orkhans@matrix:~$ ansible all -m shell -a "uptime" 192.168.37.76 | SUCCESS | rc=0 >> 12:09:55 up 36 days, 51 min, 3 users, load average: 0,31, 0,25, 0,19 192.168.37.161 | SUCCESS | rc=0 >> 12:09:55 up 17 min, 2 users, load average: 0,00, 0,02, 0,07
In this command we have -a (arguments) option, followed by the argument itself (“uptime”). We need to tell shell module which command exactly we want to run on a remote machine, therefore we have to specify an argument.
We got response from both of our servers as shown in the output.
copy module
Let’s create a file geekstuff.txt on a control server and copy it to each of the managed machines.
orkhans@matrix:/home$ ll total 12 drwxr-xr-x 3 root root 4096 Sep 21 12:24 ./ drwxr-xr-x 24 root root 4096 Sep 11 06:02 ../ drwxr-xr-x 23 administrator administrator 4096 Sep 21 12:24 administrator/ -rw-r--r-- 1 root root 0 Sep 21 12:24 geekstuff.txt
Pay attention to the argument string, it contains two arguments: src and dest. Different modules may require different number of arguments.
orkhans@matrix:~$ ansible all -m copy -a "src=/home/geekstuff.txt dest=/home/administrator" 192.168.37.76 | SUCCESS => { "changed": true, "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "dest": "/home/administrator/geekstuff.txt", "gid": 1000, "group": "administrator", "md5sum": "d41d8cd98f00b204e9800998ecf8427e", "mode": "0664", "owner": "administrator", "size": 0, "src": "/home/administrator/.ansible/tmp/ansible-tmp-1537519180.18-121800490685615/source", "state": "file", "uid": 1000 } 192.168.37.161 | SUCCESS => { "changed": true, "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "dest": "/home/administrator/geekstuff.txt", "gid": 1000, "group": "administrator", "md5sum": "d41d8cd98f00b204e9800998ecf8427e", "mode": "0664", "owner": "administrator", "size": 0, "src": "/home/administrator/.ansible/tmp/ansible-tmp-1537519180.19-69335867472705/source", "state": "file", "uid": 1000 }
We get a response containing some details including this field “changed”: true. This means that the state has been changed, that is the file has been created. If we run the same command again we will still get response with SUCCESS, but “changed” will be set to “false”, because file has been already created and nothing changed.
You can login to the remote servers if you like and make sure that the file has really been created.
file module
Now let’s delete the files we have previously created. We use file module and specify full path and state arguments.
State=absent instructs Ansible that file should be deleted. Again, we get “changed”: true, because files have been deleted. Running this command again will have no effect and return “changed”: false.
orkhans@matrix:~$ ansible all -m file -a "path=/home/administrator/geekstuff.txt state=absent" 192.168.37.76 | SUCCESS => { "changed": true, "path": "/home/administrator/geekstuff.txt", "state": "absent" } 192.168.37.161 | SUCCESS => { "changed": true, "path": "/home/administrator/geekstuff.txt", "state": "absent" }
apt module
Now let’s install tree software on the remote machines. Tree is a simple program that produces a depth-indented listing of files. I will use apt module because my remote machines are Ubuntu servers, there’s also yum module for Red Hat/CentOS. Please, note that we have the following options in this command:
-b – stands for “become”. Used to run the command as a root on a remote machine, because software installation requires root privileges.
–ask-become-pass – when using this option we will be prompted for the root password of remote machine
orkhans@matrix:~$ ansible all -m apt -a "name=tree state=installed" -b --ask-become-pass SUDO password: [DEPRECATION WARNING]: State 'installed' is deprecated. Using state 'present' instead.. This feature will be removed in version 2.9. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. 192.168.37.76 | SUCCESS => { "cache_update_time": 1537535095, "cache_updated": false, "changed": true, "stderr": "", "stderr_lines": [], "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following NEW packages will be installed:\n tree\n0 upgraded, 1 newly installed, 0 to remove and 309 not upgraded.\nNeed to get 40.6 kB of archives.\nAfter this operation, 138 kB of additional disk space will be used.\nGet:1 http://az.archive.ubuntu.com/ubuntu xenial/universe amd64 tree amd64 1.7.0-3 [40.6 kB]\nFetched 40.6 kB in 0s (269 kB/s)\nSelecting previously unselected package tree.\r\n(Reading database ... \r(Reading database ... 5%\r(Reading database ... 10%\r(Reading database ... 15%\r(Reading database ... 20%\r(Reading database ... 25%\r(Reading database ... 30%\r(Reading database ... 35%\r(Reading database ... 40%\r(Reading database ... 45%\r(Reading database ... 50%\r(Reading database ... 55%\r(Reading database ... 60%\r(Reading database ... 65%\r(Reading database ... 70%\r(Reading database ... 75%\r(Reading database ... 80%\r(Reading database ... 85%\r(Reading database ... 90%\r(Reading database ... 95%\r(Reading database ... 100%\r(Reading database ... 246667 files and directories currently installed.)\r\nPreparing to unpack .../tree_1.7.0-3_amd64.deb ...\r\nUnpacking tree (1.7.0-3) ...\r\nProcessing triggers for man-db (2.7.5-1) ...\r\nSetting up tree (1.7.0-3) ...\r\n", "stdout_lines": [ "Reading package lists...", "Building dependency tree...", "Reading state information...", "The following NEW packages will be installed:", " tree", "0 upgraded, 1 newly installed, 0 to remove and 309 not upgraded.", "Need to get 40.6 kB of archives.", "After this operation, 138 kB of additional disk space will be used.", "Get:1 http://az.archive.ubuntu.com/ubuntu xenial/universe amd64 tree amd64 1.7.0-3 [40.6 kB]", "Fetched 40.6 kB in 0s (269 kB/s)", "Selecting previously unselected package tree.", "(Reading database ... ", "(Reading database ... 5%", "(Reading database ... 10%", "(Reading database ... 15%", "(Reading database ... 20%", "(Reading database ... 25%", "(Reading database ... 30%", "(Reading database ... 35%", "(Reading database ... 40%", "(Reading database ... 45%", "(Reading database ... 50%", "(Reading database ... 55%", "(Reading database ... 60%", "(Reading database ... 65%", "(Reading database ... 70%", "(Reading database ... 75%", "(Reading database ... 80%", "(Reading database ... 85%", "(Reading database ... 90%", "(Reading database ... 95%", "(Reading database ... 100%", "(Reading database ... 246667 files and directories currently installed.)", "Preparing to unpack .../tree_1.7.0-3_amd64.deb ...", "Unpacking tree (1.7.0-3) ...", "Processing triggers for man-db (2.7.5-1) ...", "Setting up tree (1.7.0-3) ..." ] } 192.168.37.161 | SUCCESS => { "cache_update_time": 1537535097, "cache_updated": false, "changed": true, "stderr": "", "stderr_lines": [], "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following NEW packages will be installed:\n tree\n0 upgraded, 1 newly installed, 0 to remove and 355 not upgraded.\nNeed to get 40.7 kB of archives.\nAfter this operation, 105 kB of additional disk space will be used.\nGet:1 http://az.archive.ubuntu.com/ubuntu bionic/universe amd64 tree amd64 1.7.0-5 [40.7 kB]\nFetched 40.7 kB in 0s (292 kB/s)\nSelecting previously unselected package tree.\r\n(Reading database ... \r(Reading database ... 5%\r(Reading database ... 10%\r(Reading database ... 15%\r(Reading database ... 20%\r(Reading database ... 25%\r(Reading database ... 30%\r(Reading database ... 35%\r(Reading database ... 40%\r(Reading database ... 45%\r(Reading database ... 50%\r(Reading database ... 55%\r(Reading database ... 60%\r(Reading database ... 65%\r(Reading database ... 70%\r(Reading database ... 75%\r(Reading database ... 80%\r(Reading database ... 85%\r(Reading database ... 90%\r(Reading database ... 95%\r(Reading database ... 100%\r(Reading database ... 128560 files and directories currently installed.)\r\nPreparing to unpack .../tree_1.7.0-5_amd64.deb ...\r\nUnpacking tree (1.7.0-5) ...\r\nSetting up tree (1.7.0-5) ...\r\nProcessing triggers for man-db (2.8.3-2) ...\r\n", "stdout_lines": [ "Reading package lists...", "Building dependency tree...", "Reading state information...", "The following NEW packages will be installed:", " tree", "0 upgraded, 1 newly installed, 0 to remove and 355 not upgraded.", "Need to get 40.7 kB of archives.", "After this operation, 105 kB of additional disk space will be used.", "Get:1 http://az.archive.ubuntu.com/ubuntu bionic/universe amd64 tree amd64 1.7.0-5 [40.7 kB]", "Fetched 40.7 kB in 0s (292 kB/s)", "Selecting previously unselected package tree.", "(Reading database ... ", "(Reading database ... 5%", "(Reading database ... 10%", "(Reading database ... 15%", "(Reading database ... 20%", "(Reading database ... 25%", "(Reading database ... 30%", "(Reading database ... 35%", "(Reading database ... 40%", "(Reading database ... 45%", "(Reading database ... 50%", "(Reading database ... 55%", "(Reading database ... 60%", "(Reading database ... 65%", "(Reading database ... 70%", "(Reading database ... 75%", "(Reading database ... 80%", "(Reading database ... 85%", "(Reading database ... 90%", "(Reading database ... 95%", "(Reading database ... 100%", "(Reading database ... 128560 files and directories currently installed.)", "Preparing to unpack .../tree_1.7.0-5_amd64.deb ...", "Unpacking tree (1.7.0-5) ...", "Setting up tree (1.7.0-5) ...", "Processing triggers for man-db (2.8.3-2) ..." ] }
You can see that tree program was successfully installed.
You can find the list of available modules on Ansible’s website.
Conclusion
In this short tutorial you have learned how to perform basic setup of Ansible and run some ad-hoc commands.