Ansible Getting Started Guide
Overview
Automation is one of the most critical areas of improvement in most organizations. Today, most companies are in the process or re-inventing themselves in one way or another to add software development capabilities and as such, take full advantage of the digitalization of everything. Software development release cycles are changing in order to release faster. Continuous delivery, where every change is potentially it's own release is becoming the new standard. Infrastructure is following suit, after all, continuous delivery is not about just software changes but all changes and infrastructure plays a key roll. For any of this to work of course, 100% automation is required. To achieve that goal, an automation language that is easy and applicable to development and operations is needed. Ansible is that language and if you are not on-board yet, now is your chance not to miss the train because it is leaving the station. Ansible is easy, Ansible is powerful and Ansible is flexible. This guide will show that and get you up and running with Ansible before your coffee gets cold.
In this article we will dive into Ansible and learn the basics to get up and running faster, creating playbooks the right way. This article will focus on those who are new to Ansible but it may also provide value to those that are already writing their own playbooks.
Pre-requisites
- Several VMs or hosts, I recommend 5. One to be Ansible management host and other four managed nodes.
- RHEL or CentOS 7.3
Ansible Basics
- Ansible Engine - The Ansible runtime and CLI which executes playbooks. The engine runs on a management node, from where you want to drive automation.
- Playbook - A series or group of tasks that automate a process. Playbooks are written in YAML.
- Inventory - Determines what hosts you want to run playbook against. Inventories can be dynamic or static.
- Configuration - The configuration for Ansible is stored in ansible.cfg by default under /etc/ansible/ansible.cfg. It determines default settings for Ansible, where the inventory is located and much more. You can have many ansible.cfg files.
- Role - Ansible way to build re-usable automation components. A role groups together playbooks, templates, variables and everything needed to automate a given process in a pre-packaged unit of work.
- Ansible Galaxy - Ansible roles provided by the Ansible community. Not only can you get access to 1000s of roles but you can share as well.
- Ansible Modules - Used in Ansible to perform tasks, install software, enable services, etc. Modules are provided with Ansible (core modules) and also provided by the community. They are mostly written in python which is used by Ansible itself.
- Ansible Tower - API and UI for Ansible to handle scheduling, reporting, visualization and support teamwork for organizations running many playbooks across many teams.
All exercises and examples should be carried out on the Ansible management node.
Install Ansible
In order to install Ansible you need minimum python 2.6 or greater.
Check Python version.
# yum list installed python
Create Ansible user.
# useradd ansible -h /home/ansible
# passwd ansible
Install Ansible.
$ sudo yum install -y ansible
Create Inventory file.
The inventory file stores managed nodes and is used to execute playbooks or commands against a group of hosts. You can have nested groups and add variables but let's just start with a simple inventory.
$ vi inventory [servers] server1.lab.com server2.lab.com server3.lab.com server4.lab.com
ADHOC Commands
Ansible can be used to run adhoc commands against an inventory of servers. We will use Ansible adhoc commands to create a user on our four managed nodes for Ansible that can sudo.
Create Ansible user on managed nodes.
$ ansible -m user -a 'name=ansible shell=/bin/bash generate_ssh_key=yes \ state=present' -u root --ask-pass -i ./inventory servers
The above command uses the Ansible user module to create a new user on the four hosts listed in our inventory group servers. It will use root as ssh user and prompt for root password.
Create a suders file to allow ansible sudo permissions.
$ ansible -m lineinfile -a "dest=/etc/sudoers.d/ansible \ line='ansible ALL=(ALL) NOPASSWD: ALL' create=yes state=present" \ -u root --ask-pass -i ./inventory servers
The above command will use the lineinfile module to create the sudoers file for Ansible and add the line to allow Ansible user sudo permissions.
You may be interested in running other modules. Thankfully Ansible modules are wonderfully documented.
Generate list of all Ansible modules.
$ ansible-doc -l
Read documentation for user module.
$ ansible-doc user
Now we come to best practice #1. You might discover a module called command or shell. These modules can be used to execute any remote commands. While convenient, always try to find a purpose based module instead of using command or shell. Using specific modules is much cleaner, more readable and should always be the first choice.
Playbook Basics
Now that we understand how inventories work and how to run basic adhoc commands it is time to explore playbooks. In order to run a playbook you need Ansible, an inventory, a remote user and of course the playbook itself. A playbook simply put is just one or more tasks. Above we saw how to execute individual adhoc commands or tasks using our inventory servers. Now let us bring those tasks together in a playbook.
Create playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers tasks: - name: Add a user user: name: ansible shell: /bin/bash generate_ssh_keys: yes state: present - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ansible line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present
Now we come to best practice #2. The name parameter is optional, don't be lazy, always name the playbook and all tasks. This is important for reading what was done and also to let other's know what your playbook is supposed to be doing.
You can see how the inventory group is used and also how the tasks are invoked. Again a task just invokes and Ansible module with a set of parameters. Everything is documented in ansible-doc. With just what we have covered here, probably took you 10-15 minutes you can start automating everything with Ansible. Hopefully a light as gone on.
Run playbook.
Assuming you ran the adhoc commands a ansible user already exists that can sudo. If not you need to run the adhoc commands.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
We can use the newly created Ansible user and the -b option (become root) to elevate permissions.
Variables
Variables allow for dynamic input. In Ansible variables can be defined in many ways such as: playbook itself, inventory file via host or group vars, cli via extra_vars or in a separate vars file that can be included in playbook. Let's take a look at these options in more detail. Instead of hard-coding the username we created above let's explore doing so using the different variable options.
Vars via inventory file.
Variables can be defined for a host or group in the inventory file itself. Here we will define a variable username for the group servers.
Update inventory file and add vars for group servers.
$ vi inventory [servers] server1.lab.com server2.lab.com server3.lab.com server4.lab.com [servers:vars] username=ansible
Update playbook to use variables.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers tasks: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present
Notice that the variable needs quotes only when it starts a line.
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Variables in playbook
Variables can be defined directly in the playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars: username: ansible tasks: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Variables imported from vars file.
Similar to above example, variables can be defined in a separate file and then imported into playbook.
Create vars file.
$ vi my_vars.yml --- username: ansible
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present
The best practice #3 is to use inventory file to define key/value type variables that should be parameterized. Variables that are dynamically generated or utilize nested data structures should use vars_files and be included. Avoid using vars in playbook directly if possible.
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Ansible Facts
Each time Ansible is run unless disabled the setup module is also run. The setup module gathers Ansible facts. These are variables that give us valuable information about the managed host and they can be acted upon within playbooks. Anything from a hosts network, hardware and OS information are gathered. It is also possible to define custom facts that would be gathered.
View ansible facts for a host.
$ ansible -m setup -u ansible -b -i ./inventory servera.lab.com
Use ansible facts in playbook.
Here we will print the memory and number of cpu cores in our playbook by adding a new task.
$ vi add_user.yml --- - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." ---
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Controlling Playbooks
We have seen how to use modules to drive tasks and even parameterize them with variables. Next we will understand how to better control tasks within those playbooks.
When Conditional
This acts as an if statement in common programming languages. In Ansible we use when statement to run a task based on a condition being met.
Execute add user only when username is defined.
$ vi add_user.yaml --- - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present when: username is defined ---
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Loops
In Ansible loops are very useful when the same task or module need to execute against a list. In our playbook let's update it to take action on many users and thus show how to use a loop.
Update vars file.
$ vi my_vars.yml --- users: - ansible - bob - joe - keith
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Handlers
In order to couple tasks or have a task executed from another task, a handler is required. Here we will look at converting the "configure sudo permission" task into a handler that can be triggered by the "add a user" task.
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined notify: - 'Configure sudo permission' handlers: - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
Now sudo permissions will only be set when a new user is actually added. It is very important to understand behavior with handlers, a notifer will only run when the task made a change.
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
Tags
It may be desired to only run certain tasks and to control which tasks get run from cli. This is done in Ansible via tags. Lets set a tag which will only run the task that prints memory and cpu info.
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." tags: - info - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined notify: - 'Configure sudo permission' handlers: - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
Run playbook using tag 'info'.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory --tags 'info'
Blocks
In order to handle errors in Ansible blocks are often used. The block statement defines the main task to run. The rescue statement defines a task that should run if an error is encountered inside the block. Here we will setup error handling for our add user task.
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." tags: - info - block: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined notify: - 'Configure sudo permission' rescue: - name: revert user add user: name: "" state: absent with_items: "" when: item is defined handlers: - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
The playbook will now attempt to remove users if an error occurs with the add user task. You can simulate this by adding a new user to my_vars.yml file and running without the -b (become root) option.
Run playbook.
$ ansible-playbook add_user.yml -u ansible -i ./inventory
Templates
In Ansible templates are used mostly to parameterize configuration files. Ansible uses the jinja2 templating system. A template module is provided that defines the template and where it should be placed. Variables are automatically substituted for their values when the template is copied to its destination. We will add a new task to the playbook which sets the motd using hostname as a dynamic variable from a motd jinja2 template.
Create a jinja2 template for motd.
$ vi motd.j2 Ansible Rocks!!! This is host .
Update playbook.
$ vi add_user.yml --- - name: Add a privileged user for Ansible hosts: servers vars_files: - ./my_vars.yml tasks: - name: Configure MOTD using template template: src: ./motd.j2 dest: /etc/motd owner: root group: root mode: 0644 - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." tags: - info - block: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined notify: - 'Configure sudo permission' rescue: - name: revert user add user: name: "" state: absent with_items: "" when: item is defined handlers: - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
Run playbook.
$ ansible-playbook add_user.yml -u ansible -b -i ./inventory
The playbook will now update motd based on our jinja2 template. The hostname will be dynamically applied based on the variable.
Check MOTD.
$ ssh -l ansible server1 Ansible Rocks!!! This is host server1.
Roles
Now that we have a good understanding about playbooks it is time to move on to roles. In Ansible roles allow Ansible to be reusable. The Ansible community provides galaxy which allows community members to share roles. There are 1000s of roles so often if you want to do something in Ansible, there is probably already a roll that exists in galaxy to do so. You can also easily modify roles to fit intended purpose. We now come to best practice #4, don't be lazy, always create roles. Not only will roles allow you to share your playbook packages with others, but will also enforce good structuring , clean code and allow you to re-use role components in ways that were not even considered. We will now restructure our playbook tasks, variables, handlers and templates into a add user role.
Create empty role structure.
The ansible-galaxy command allows lists, installs and removes roles from galaxy. It also generates the empty structure of a role, useful for creating your own roles. Here we will create the empty structure for our new add_user role.
$ ansible-galaxy init --offline -p roles add_user
Create role tasks.
Now we will move the tasks from our playbook into the role. A simply copy/paste and delete of the first four empty spaces of each line will suffice.
$ vi roles/add_user/tasks/main.yml --- - name: Configure MOTD using template template: src: ./motd.j2 dest: /etc/motd owner: root group: root mode: 0644 - name: Print Memory and CPU Cores debug: msg: "Host has MB Memory and CPU Cores." tags: - info - block: - name: Add a user user: name: "" shell: /bin/bash generate_ssh_keys: yes state: present with_items: "" when: item is defined notify: - 'Configure sudo permission' rescue: - name: revert user add user: name: "" state: absent with_items: "" when: item is defined
Create role handlers.
Similar to tasks handlers also have a specific location within a role. Again copy/paste and delete of the first four spaces of each line.
$ vi roles/add_user/handlers/main.yml --- - name: Configure sudo permission lineinfile: dest: /etc/sudoers.d/ line: 'ansible ALL=(ALL) NOPASSWD: ALL' create: yes state: present with_items: "" when: item is defined
Create role vars.
Variables for a role can be stored under vars or default. In this case we will put them under vars.
$ vi roles/add_users/vars/main.yml --- users: - ansible - bob - joe - keith
Create role templates.
Templates simply need to be copied to the correct location.
$ cp motd.j2 roles/add_user/templates
Create playbook that uses role.
$ vi add_user_using_role.yml --- - name: Add user using role hosts: servers roles: - add_user
Run playbook.
$ ansible-playbook add_user_using_role.yml -u ansible -b -i ./inventory
The playbook will now execute our role. As you have seen roles provide a pre-defined structuring and packaging of playbook tasks that greatly enhances re-usability.
Summary
In this article we have discussed the need and great opportunity automation provides. We have explored the basics of Ansible and why Ansible has become the standard language of automation. This article attempts to provide a pragmatic approach to learning Ansible, from installation, adhoc commands, creating your first playbook , learning fundamentals and finally putting it all together in a role. The greatest thing you can do for yourself and your organization is to automate everything.
Happy Automating!
(c) 2017 Keith Tenzer