Ansible Getting Started Guide

31 minute read

ansible-logo

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