Ansible is an orchestration tool with endless automation possibilities, that has transformed and revolutionized the world of Application deployment, Task automation and Configuration management. Ansible is written in python and is also available as a python library. The things people enjoy about Ansible is that it is fairly simple to understand & use, and it does not need an agent software running on your target machines where you want the task or deployment to happen. It makes use of the SSH protocol to communicate with its target systems.
Ansible can be installed on any machine that has a python 2.x or above, however the examples and references in this post as well as this series, will be based on a CentOS 7 machine running Python 2.7.5.
Installing Ansible in a virtual environment is recommended for, it enables you to keep the maintenance of your Ansible code and python dependencies independent of the system on which it is running. However, Ansible installation can also be as simple as running the command “yum install ansible”, once EPEL repository is enabled.
Ansible Inventory
Ansible inventory file, as the name suggests is a file used to store a list of host names on which tasks can be run remotely and in parallel. These tasks can be anything ranging from copying files, installing software or rebooting machines to collecting reports from the remote systems, deploying large applications etc. The Ansible inventory can be as simple as a list of host names separated by a new line or it can also be multiple groups of host names with variables specific to both groups and individual host names expressed in the form of yaml or ini. The ini version being the simplest and commonly used format, let’s look an example. Let’s suppose you have a few Linux machines in your company, and a few of them serve as an application hosts and a few serve as database hosts. You would want your database related tasks to be run on a group that only contains host names running a database and application related tasks on a group that only contains hosts running applications, and so on. You might also have some tasks that need to be run on both groups, such as OS updates.
admin_server1 admin_server2 [application_servers] server_java_app1 server_php_app1 server_php_app2 [DB_servers] server_oracle_1 server_mysql_1 [application_servers:children] thirdparty_applications secondparty_applications [thirdparty_applications] ftp_server_app1 monitoring_server_app [secondparty_applications] marketing_server_app
In the above ini formatted inventory, we define two admin servers that’re not classified under any group. We have two major groups namely application_servers and DB_servers containing a few hosts. The group ‘application_servers’ also has two child groups containing hosts running third party and second party applications. Child groups are defined in the format [parent_group:children].
Ansible variables: group vars and host vars
Like in any programming language, Ansible variables can store information, which can be referenced while running the program. For instance, in order to relate to the above inventory example, let’s suppose you have different primary DNS servers serving application hosts and database hosts and a common secondary DNS sever for all groups. The inventory file for such a scenario would look like this.
admin_server1 admin_server2 [application_servers] server_java_app1 server_php_app1 server_php_app2 [DB_servers] server_oracle_1 server_mysql_1 [application_servers:children] thirdparty_applications secondparty_applications [thirdparty_applications] ftp_server_app1 monitoring_server_app [secondparty_applications] marketing_server_app [application_servers:vars] primary_dns=10.65.1.4 [DB_servers:vars] primary_dns=11.65.1.4 [all:vars] secondary_dns=172.65.1.3
Note that, variables are defined in the format [group_name:vars]. These variables are called ‘group_vars’ in Ansible, since these variables are common to a group of systems. Variables defined under [all:vars] is common to all hosts in the inventory. ‘all’ is Ansible’s predefined group and any host in the inventory will automatically fall under this group.
Let’s suppose one of your database hosts ‘server_oracle_1’ is such that it has a network address that does not allow it to reach both primary and secondary DNS servers (11.65.1.4 & 11.65.1.5) defined in its ‘group_vars’. Suppose, there is another dedicated primary and secondary DNS server available in its range that you’d like to define in the inventory for this host only. In this case, we can define a DNS server specific to this host in the inventory, without having to create a new group with just one host in it. These variables that are host specific, are called ‘host_vars’ in Ansible. Let’s update the inventory with the host variables for primary_dns and secondary_dns for host ‘server_oracle_1’.
admin_server1 admin_server2 [application_servers] server_java_app1 server_php_app1 server_php_app2 [DB_servers] server_oracle_1 primary_dns='172.16.1.1' secondary_dns='172.16.1.2' server_mysql_1 [application_servers:children] thirdparty_applications secondparty_applications [thirdparty_applications] ftp_server_app1 monitoring_server_app [secondparty_applications] marketing_server_app [application_servers:vars] primary_dns=10.65.1.4 [DB_servers:vars] primary_dns=11.65.1.4 [all:vars] secondary_dns=172.65.1.3
Note: Host variables take precedence over group variables, and thus when a variable name is referenced, and if the variable is defined under both group_vars and host_vars, then host_vars is applied.
Listing entries from the inventory file is possible, by running command “ansible-inventory -i path/to/inventory_file –list”. This command resolves the dependency between child & parent groups, resolves variable precedence and outputs the entries in a Json format (a lightweight format for storing and transporting data). In other words, this command prints the contents of the inventory in a pretty format. Below is the sample output of this command on our inventory file. This also sort of validates the inventory for any errors.
Note: when a path to inventory file is not specified with ‘-i ‘, Ansible looks for entries in /etc/ansible/hosts, which is the default Ansible. inventory file
[admin@utilxutils~]# ansible-inventory -i myinventory.ini --list { "DB_servers": { "hosts": [ "server_mysql_1", "server_oracle_1" ] }, "_meta": { "hostvars": { "admin_server1": { "secondary_dns": "172.65.1.3" }, "admin_server2": { "secondary_dns": "" }, "ftp_server_app1": { "primary_dns": "10.65.1.4", }, "marketing_server_app": { "primary_dns": "10.65.1.4", "secondary_dns": "10.65.1.5" }, "monitoring_server_app": { "primary_dns": "10.65.1.4", "secondary_dns": "10.65.1.5" }, "server_java_app1": { "primary_dns": "10.65.1.4", "secondary_dns": "10.65.1.5" }, "server_mysql_1": { "primary_dns": "11.65.1.4", "secondary_dns": "11.65.1.5" }, "server_oracle_1": { "primary_dns": "172.16.1.1", "secondary_dns": "172.16.1.2" }, "server_php_app1": { "primary_dns": "10.65.1.4", "secondary_dns": "10.65.1.5" }, "server_php_app2": { "primary_dns": "10.65.1.4", "secondary_dns": "10.65.1.5" } } }, "all": { "children": [ "DB_servers", "application_servers", "ungrouped" ] }, "application_servers": { "children": [ "secondparty_applications", "thirdparty_applications" ], "hosts": [ "server_java_app1", "server_php_app1", "server_php_app2" ] }, "secondparty_applications": { "hosts": [ "marketing_server_app" ] }, "thirdparty_applications": { "hosts": [ "ftp_server_app1", "monitoring_server_app" ] }, "ungrouped": { "hosts": [ "admin_server1", "admin_server2" ] } } [admin@unixutils ~]#
Running Ad-Hoc remote commands
Now that we have a valid inventory in hand, let’s try to run a command on some systems from the inventory. Commands can be run on either a single host or a group. For example, let’s try to run the command “uname -a”.
command: ansible -i <inventory_file> <hostname OR group_name> -a <some_command> -u <user_name> -k
NOTE: -k argument is to make Ansible ask for a password for authentication). If this is not specified, Ansible will attempt a public key authentication on target systems. In case your command needs to run with sudo permissions you can additionally pass -K (in caps), so that Ansible will also prompt for a sudo password.
Output:
[admin@unixutils ~]# ansible -i myinventory.ini admin_server1 -a "uname -a" -u root -k SSH password: admin_server1 | CHANGED | rc=0 >> Linux deployment 3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux [admin@unixutils ~]#
[admin@unixutils ~]# ansible -i myinventory.ini DB_servers -a "uname -a" -u root -k SSH password: server_oracle_1 | CHANGED | rc=0 >> Linux deployment 3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux server_mysql_1 | CHANGED | rc=0 >> Linux deployment 3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux [admin@unixutils ~]#
From the output, we see that the ‘uname -a’ command is run successfully on individual host and the second command, show that it was run on the group ‘DB_servers’. In order to understand how host_vars and group_vars can be used, let’s try to use it in an example.
The below command will make the remote systems ping the DNS that was defined for it in the inventory file.
Command: ansible -i myinventory.ini DB_servers -a “ping -c 1 {{ primary_dns }}” -u root -k. This command should automatically pick up the value for primary_dns from host_vars if defined, else from group_vars, from the inventory file myinventory.ini.
NOTE: In Ansible, variables are referenced in the format {{ variable_name }}.
Output:
[admin@unixutils ~]# ansible -i myinventory.ini DB_servers -a "ping -c 1 {{ primary_dns }}" -u root -k SSH password: server_oracle_1 | CHANGED | rc=0 >> PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data. 64 bytes from 11.65.1.4: icmp_seq=1 ttl=64 time=0.065 ms --- 10.0.3.15 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.065/0.065/0.065/0.000 ms server_mysql_1 | CHANGED | rc=0 >> PING 11.65.1.4 (11.65.1.4) 56(84) bytes of data. 64 bytes from 11.65.1.4: icmp_seq=1 ttl=64 time=0.065 ms --- 11.65.1.4 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.065/0.065/0.065/0.000 ms [admin@unixutils ~]#
[admin@unixutils ~]# ansible -i myinventory.ini admin_server1 -a "ping -c 1 {{ secondary_dns }}" -u root -k SSH password: admin_server1 | CHANGED | rc=0 >> PING 172.65.1.3 (172.65.1.3) 56(84) bytes of data. 64 bytes from 172.65.1.3: icmp_seq=1 ttl=64 time=0.065 ms --- 172.65.1.3 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.065/0.065/0.065/0.000 ms [admin@unixutils ~]#
In this post we had an overview of how to write an Ansible inventory file with variables. We also tried our hands on running commands on the systems from our inventory file, remotely. We did not use any of the Ansible modules for running tasks remotely, but we passed simple ad-hoc commands as arguments to Ansible. Ansible has a ton of inbuilt modules and is also being developed constantly by the community. These modules can be invoked to make our life ease. For instance, lets suppose you would like to install a list of software on the remote Red hat systems and restart some services. Traditionally, you’d need a script to get such tasks done. Instead, you can invoke the Ansible’s yum module and pass the list of software to it and get it installed, and the service module to start/stop/restart services, without having to use special scripts. We will look at how to invoke Ansible’s modules and make our life simpler, in future posts.