Ansible is a simple agent-less automation tool that has changed the world for the better. It has many use cases and wide adoption (used by many upstream projects like Kubernetes and there are thousands of rules submitted to Ansible Galaxy). In this article, we are going to demonstrate Ansible. The intention of this article is not to teach you the basics of Ansible, but to motivate you to learn it.
Why is Bash scripting not automation?
Shell has been the comfort zone of every single sysadmin I know. They think, dream, and curse in BASH. While BASH would continue to be one of the basic requirements of every sysadmin, one needs to leverage his/her skills into real automation beyond shell scripting. I believe Ansible should become part of the comfort zone of every sysadmin and developer.
Imperative vs. declarative
Besides being an interactive shell where we type commands on the command line, BASH serves as an interpreted programming language. And as any of such languages, it's "imperative" which means your code is telling the interpreter ordered steps to execute one by one, do this then do that, then check this and if so do this and that, then go over to elsewhere and do this and that. On the other hand, declarative domain-specific languages (DSL) do not specify ordered steps but they describe the desired state. An example of this is the nginx configuration below (which says pass the request to port 8000 and set those two headers and of course the headers would be set before the proxy_pass despite being written after it) that's the reason imperative logic like "if" does not give the expected results, they call it "if is evil".
location / { proxy_pass http://localhost:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
Because Ansible is declarative, it's very simple and to the point "Get me this thing done" and "Put this in that desired state".
From anywhere to the desired state
Given a server in state A, we write a script to put it in state B. If you run the script twice usually it won't work because in the second run it would be starting from state B which is not what it was written for. For example creating a user would fail because the user is already there. Similarly creating a run-time directory. If you have many assumptions for state A that your automation script should expect and handle, the more generic the assumptions the more logic you have to put to handle all kinds of possibilities.
On the other hand, Ansible is all about the desired state, each task defined in ways regardless of context or initial state, for example, the following YAML task means "I want a user named Omar to be present, and the user named Yousef to be absent":
- user: name=omar state=present - user: name=yousef state=absent
You can refer to Ansible user module for more details, another example is from Ansible file module:
- file: path=/home/omar/tmp state=directory owner=omar group=admin mode=0775
No matter from which state you start, no matter what are the steps leading to that desired state you get what you want. Ansible reports if it was able to successfully put it in the desired state or not and it also reports if a change was made. For example, the user Omar might already exist, it would report that it was a success without change. Doing the second example in bash means that you have to check if the directory exists, check who is the owner user and owner group and what is the current permission of the file and report if any change is necessary and do it.
Dry run and report changes
Being able to go from the desired state of itself might sound ridiculous but it's used as "dry run" to report changes or deviation from the desired state. It can also be used periodically to enforce some policy. Below is an example of how to run an Ad-Hoc ansible command which creates a directory /tmp/mydir with given properties.
$ ansible -m file -a 'path=/tmp/mydir state=directory owner=root group=wheel mode=0775' localhost
If you run it, it would fail, because I don't have permission to do so, to fix this let's add "--become --ask-become-pass" or "-b -K" for short which would successfully create that directory.
$ ansible -b -K -m file -a 'path=/tmp/mydir state=directory owner=root group=wheel mode=0775' localhost SUDO password: [WARNING]: provided hosts list is empty, only localhost is available localhost | SUCCESS => { "changed": true, "gid": 10, "group": "wheel", "mode": "0775", "owner": "root", "path": "/tmp/mydir", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 40, "state": "directory", "uid": 0 }
But the good part is that you can add "--check" (or "-C" for short) to just report if change is needed.
$ ansible -C -m file -a 'path=/tmp/mydir state=directory owner=root group=wheel mode=0775' localhost [WARNING]: provided hosts list is empty, only localhost is available localhost | SUCCESS => { "changed": false, "gid": 10, "group": "wheel", "mode": "0775", "owner": "root", "path": "/tmp/mydir", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 40, "state": "directory", "uid": 0 }
Working on multiple servers at once
If you were asked to report git branch that all servers are standing on. One way is to login into every server and type "git rev-parse --abbrev-ref HEAD" but if you have tens on hundreds of servers that won't be a funny task. In the past, I was in love with python fabric, but writing a python code for your day to day tasks is also not productive. For this task, a simple ansible command using shell module would do the job.
$ ansible -i 10.0.0.2,10.0.0.3,10.0.0.4, -b -u myuser --become-user=proj -m shell -a "cd ~proj/repo/ && git rev-parse --abbrev-ref HEAD" all
As you can see we specified the list of IPs we are going to work on using "-i" short for "Inventory" which is either a file name or a comma separated list of IPs ending in a comma. Instead of remembering the IPs we can place them in a file.
10.0.0.2 10.0.0.3 [web] 10.0.0.3 10.0.0.4 [db] 10.0.0.5
You might call those file hosts and pass it to "-i" and you can also pass "all" or "web" or "db" to run the command on.
We have specified the remote user to be used to connect to those servers with -u and the user to sudo before running the command using "--become-user".
Readable Playbooks
You think BASH scripts are readable, think again as I mean those written by others. If you download Apache Solr or Apache Cassandra you see a bash script that just exports some environment variables then execute "java $OPTIONS -jar file.jar", despite being a simple task the script is not readable by all means. On the other hand, if you look at the Ansible playbooks that deploy a complex highly available cluster, you would see that it's very simple and readable. Let's save the file below as book1.yml.
--- - hosts: all become: yes tasks: - user: name=omar state=present - user: name=yousef state=absent - file: path=/tmp/mydir state=directory owner=root group=wheel mode=0775
To run the playbook above you type "ansible-playbook -i ./hosts book1.yml".
When you become comfortable with Ad-Hoc commands, you should start writing playbooks instead of shell scripts, it's a simple YAML here is a fully functional playbook that installs Apache web server (called "httpd" on RedHat family and "apache2" on Debian family) using suitable package manager and make sure it's enabled and started and it places a simple index.html page.
--- - hosts: all become: yes tasks: - set_fact: httpd_pkg=httpd when: ansible_os_family == "RedHat" - set_fact: httpd_pkg=apache2 when: ansible_os_family == "Debian" - copy: dest=/var/www/html/index.html content="Hello, world!" - package: name={{httpd_pkg}} state=present - service: name={{httpd_pkg}} state=started enabled=yes
Save the file above as book2.yml and run it on your local machine using local connection type using,
$ ansible-playbook -K -c local -i localhost, book2.yml
as before "-K" is to ask for sudo password and "-b" is not needed because it's part of the file.
We have used the "ansible_os_family" provided by ansible to conditionally set another fact using when.
Orchestration
You can do parallel execution, serial execution, delegated execution, etc.
For example, you can go to the load-balancer remove a server from it, do some change to the server, then put it back to the load-balancer. You can go all Django application servers, fetch the latest code from git on all servers then only on one of them run "python manage.py, migrate" then reload the wsgi service on all servers.
Making Ansible your comfort zone
Make sure you take a glance at Ansible's list of modules by category or the grand long list of all modules.
Use Ansible Ad-Hoc commands just as you use interactive bash shell.
Learn facts reported by Ansible (run "ansible -m setup localhost | less").
Write Ansible playbooks instead of shell scripts.
Check for a module before use any command ex. "yum install", "systemctl start httpd".
Take advantage of your Red Hat Developers membership and download RHEL today at no cost.
Last updated: March 22, 2023