Contents

Ansible 101: A Beginner's Guide to IT Automation

Introduction to Ansible language and basic usage explained

Website Visitors:

Ansible is an open source automation tool which helps administrators manage their servers. It can be used to automate tasks across multiple machines on a network. In this article, we’ll explain the basic ansible definitions and their usage.

What is Ansible

Ansible is a cross-platform, agentless, automation tool using which admins can manage remote PCs. You can use Ansible for a lot of tasks like installing software, windows updates, provisioning servers in the cloud, configuration management, and more…. While mostly used for automation, it isn’t restricted to DevOps alone.

Using Ansible, you can add all your servers and workstations in a common playbook (explained in later sections) and deploy softwares to different PCs with different operating systems. Ansible files, like playbooks or var files, written in YAML language. Make sure that all the files that you write, follow YAML coding rules.

In the Ansible deployment, you have a core Ansible server called controller, which is used for managing the other hosts. This controller server will contact the Linux machines over SSH and Windows machines over PowerShell. So, as you have guessed by now, PowerShell remoting should be enabled on all the machines that you want to manage using Ansible.

Now that you know what is Ansible and what is it used for, let’s look at the Ansible concepts.

Inventory

List of servers/workstations (aka hosts) in your environment that you want to manage is an inventory. In this inventory you can specify all hosts or individual hosts or range of hosts (if your servers are named as webserver1, webserver2 and so on..). All the information about the remote host should be added in this inventory file.

In the inventory file, you can specify the hosts in different types as shown below:

Single host:

1
2
server1.company.com
server2.company.com

Group hosts as per the environment or OS type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[winservers]
server1.company.com
server2.company.com 

[linuxservers]
linuxserver1.company.com
linuxserver2.company.com 

[dbservers]
db1.company.com
db2.company.com

Add single ip address or add regular expression type syntax for specifying a range of ip addresses:

1
2
3
4
5
6
7
8
[winservers]
server1.company.com
192.168.100.100
192.168.[10-20].100

[linuxservers]
linuxserver1.company.com
linuxserver2.company.com

Using hosts with alias:

1
2
dbserver  ansible_host=db1.company.com
mailsrv  ansible_host=mailsrv.company.com

Using groups of other groups:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[WindowsServer]
Server.company.com

[WindowsClient]
win10.company.com

[Linux]
Linux1.company.com

[Windows:children]
WindowsServer
WindowsClient

In the same file, specify other parameters, as shown below. Here ansible_connection, ansible_winrm_transport etc… are Ansible keywords. For Linux you must change the ansible_connection value to SSH, change the port, username and password.

1
2
3
4
5
6
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_transport=ntlm
ansible_winrm_port=5985 (for Linux, ansible_port=22 , default SSH port)
ansible_user=administrator
ansible_password=mypassword

PlayBooks

As Playbooks are written in YAML format, having YAML knowledge is a good addon while writing them.

Playbooks contain group of plays that are executed on a set of hosts. Plays are a group of tasks that are performed on the hosts. These tasks include commands to start a service or install a software or reboot machines etc…

Sample playbook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-
  name: List Remote Servers
  hosts: server01
  tasks:
    - name: Running hostname command
      command: hostname

    - name: Install script
      command: /var/tmp/TestScript.sh

    - name: Install sshd
      apt:
        name: "openssh-server"
        state: "present"

    - name: Start sshd service
      service: 
        name: sshd
        state: started

There is no rule that you have to use all the hosts defined in the inventory. But the hosts defined in the playbook should have its associated information in the inventory file.

Module

Modules are blocks of code grouped as one, and executed in a playbook task. All the code that you write in a task will be associated with one or more modules. For example, let’s say you want to ping a server, and then run a command to install the software, and start a service. Here, ping, command and service are modules used to achieve the task. There are a lot of modules like database, files, packages, users and groups, services and more…

Variables

When you run a playbook, you may have to store the usernames, passwords to connect to the servers or a share path link which can be used later in the playbook. All this data is stored under the variable file and referred in the ansible-playbook command or used in the playbooks directly.

You can add variables in a separate file and refer it in the ansible-playbook command or add variable directly in the playbook file. When you want to use that variable, use it in curly braces with the variable name. This is called Jinja2 templating.

Place all the variables in a separate file so that you don’t have to change your playbook file every time you want to change the variables.

Sample format:

path: ‘{{software_path}}’/iso/sources

If you are starting with a variable name as shown above, you must use single quotes. If you are referencing a variable name in between a command, then single quotes are not needed.

Conditionals

You can add a condition to check whether the condition is satisfied on the server and then run the command if it is satisfied. For example, if you want to run httpd service you want to check if httpd is installed or if the host is a web server from the hosts list and so on. You have to use double equals for comparision.

1
2
when: ansible_host == "web1.company.com" or 
      ansible_host == "web2.company.com"

Register

You can also use Register module and run another action based on the output. If a service is stopped, you can capture the result and send email or restart a service based on the output of a command.

1
2
3
4
5
6
7
tasks: 
   - command: service sshd status
     register: sshd_output

   - mail:
         XXXXXXXXXXX
     when: sshd_output.stdout.find('down') != -1

Loops

When you want to perform the same task multiple times, loops is what you have to look at. Based on the task, you’d specify the command once and run it on multiple times. You can do this by using with_items keyword. This is like foreach command in other programming languages.

Examples are, you list all softwares and install one by one or get the status of services on multiple machines etc…

From Ansible 2.5, loop keyword is introduced. Even though it is not a complete replacement for with_items keyword, but it covers most of the functionality of with_items.

In the task steps, specify the list of items as shown below. In the service installation command, you have to use jinja2 template ‘{{item}}’. In below example, multiple services are installed on local PC.

1
2
3
4
with_items:
  - httpd
  - php
  - vsftpd

Tasks

Tasks are chunks of code that perform a single activity. You can use these tasks in playbook. To look at it with an example, if you want to install a web server and start the service, installing the web server is a task, starting the service is a task.

For calling tasks in scripts, we have to use include and tasks keywords and the filename.yml.

Task example:

1
2
3
4
5
- name: Sample Application
   hosts: dbserver
   tasks:
    - include: tasks/installmysql.yml
    - include: tasks/configuremysql.yml

Roles

A role enables the sharing and reuse of Ansible tasks. It contains Ansible playbook tasks, plus all the supporting files, variables, templates, and handlers needed to run the tasks. A role is a complete unit of automation that can be reused and shared.

In practical terms, a role is a directory structure containing all the files, variables, handlers, Jinja templates, and tasks needed to automate a workflow.

Let’s say you want to install mysql and configure it. There are two ways in which you can create folder structure for roles.

  • In the folder where you have your playbook file, create a folder called roles and a subfolder tasks and create installmysql and configuremysql subfolders. In installmysql folder, create file main.yml. This file will have the commands to install mysql. Next, in roles/tasks/configuremysql folder, create main.yml file. This file will have commands to configure mysql.

Folder structure: PlaybookFolder/roles/tasks/installmysql/main.yml PlaybookFolder/roles/tasks/configuremysql/main.yml

  • Using ansible-galaxy command you can create a role folder structure. In your ansible project folder, create a folder called roles. In roles directory, execute the below command.

ansible-galaxy init RoleName

This will create the folder structure as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
./defaults:
main.yml

./files:

./handlers:
main.yml

./meta:
main.yml

./tasks:
main.yml

./templates:

./tests:
inventory  test.yml

./vars:
main.yml

By default Ansible will look in each directory within a role for a main.yml file for relevant content (also main.yaml and main):

  • tasks/main.yml - the main list of tasks that the role executes.
  • handlers/main.yml - handlers, which may be used within or outside this role.
  • library/my_module.py - modules, which may be used within this role (see Embedding modules and plugins in roles for more information).
  • defaults/main.yml - default variables for the role (see Using Variables for more information). These variables have the lowest priority of any variables available, and can be easily overridden by any other variable, including inventory variables.
  • vars/main.yml - other variables for the role (see Using Variables for more information).
  • files/main.yml - files that the role deploys.
  • templates/main.yml - templates that the role deploys.
  • meta/main.yml - metadata for the role, including role dependencies.

Sample project folder structure: ../ProjectFolder/roles/RoleName/tasks/main.yml

When you want to use these roles, you have to specify roles keyword with the particular role name. This will load the tasks from tasks folder, variables from variables folder. When using roles include command is not needed.

1
2
3
4
- name: Install webserver
   hosts: linuxservers
   roles:
    - webserver

You can use this role in anywhere in the playbook.

Role example:

1
2
3
4
5
- name: Sample Application
   hosts: dbserver
   roles:
   - installmysql
   - configuremysql

In the folder where you have the playbook file, create a folder called “roles” and create folder with the task you are going to perform. In that folder create main.yml which will be the actual script file for running the task.

For tasks, we have to use include and tasks keywords and the filename.yml. But for roles, we have to use the roles keyword and the name of the role.

Different ways of adding Hosts in playbook

When working with Ansible, you need a list of all your server names in a file. This list of servers is called an Inventory. After you create an inventory file, you can group them as per their OS, i.e., Windows and Linux.

Default location of Ansible hosts file is /etc/ansible/hosts. When you run Ansible command, you can specify your own inventory file using -i parameter. There are multiple ways on how you can define your inventory file. Some options are:

  • Define all your servers one by one in the inventory file.

    Here, server1 and server2 are the alias name for your servers. Choose a word based on the server’s functionality. For web servers, it can be webserver1, webserver2 and so on.. ansible_host is your target server’s IP address and ansible_ssh_pass is the ssh password for your target server. ansible_host and ansible_ssh_pass are ansible keywords. All ansible keywords are listed here: InventoryKeywords

    1
    2
    
    server1 ansible_host=TargetServerIPaddress ansible_ssh_pass=PWD
    server2 ansible_host=TargetServerIPaddress ansible_ssh_pass=PWD
    
  • Arranging them in a group as per the server functionality. If you are working with webservers, you can group them as:

    1
    2
    3
    
    [webserver]
    webserver1 ansible_host=ip ansible_ssh_pass=PWD
    webserver2 ansible_host=ip ansible_ssh_pass=PWD
    

When selecting the hosts in the playbook file you can mention them one by one as webserver1, webserver2 or use the group name - webserver

  • Use * with your server alias. For example, In your inventory file if you have webserver1…… and webserver2….. then use webserver* in the hosts category in your playbook file.This will pick all the servers with webserver name in your inventory file.

Separating into Files

host_vars folder

If you have different variables for different servers like password or your target server’s ip address or any other variable, it would look nice to be written in one place and called in wherein needed. This is achieved by creating host_vars folder in the folder where you have your playbook file.

Create host_vars directory in the same playbook folder. host_vars folder name should be created exactly with the same name. In that folder, create files with the same name as the server name alias you used in the inventory file with yml extension. Place all your variables in that file. For each server in your inventory, create a separate file in host_vars folder. If you have 10 servers, you must create 10 files with yml extension.

Info
In inventory file you use the format, ansible_host=IPAddress. You have to use = sign in inventory file. But in the host_vars/alias.yml file, you must use the format as ansible_host: IPAddress. You should use a full colon when you use host_vars folder. This is because this file is in yaml format.

Inventory file:

1
2
3
[linuxservers]
linuxserver1 ansible_host=192.168.1.1
linuxserver2 ansible_host=192.168.1.2

host_vars folder:

1
2
3
4
5
linuxserver1.yml 
ansible_ssh_pass=PWD1

linuxserver2.yml
ansible_ssh_pass=PWD2

No other special parameters are needed. Ansible picks up host_vars folder automatically.

Group_vars folder

If you have the same variable on multiple servers, you can then create a folder called group_vars. For this, create a group in the inventory file for your servers. Next, create a folder group_vars folder in the same folder where you have the playbook file. Create a yaml file in group_vars folder with the same name as your group name in the inventory file. For example,

Inventory file:

1
2
3
[linuxservers]
linuxserver1 ansible_host=192.168.1.1
linuxserver2 ansible_host=192.168.1.2

group_vars file:

1
2
linuxservers.yml
ansible_ssh_pass=Password

filename should be linuxservers.yml as linuxservers is the group name defined in inventory file. Add all the variables to this yaml file. If both the servers have same password, you can add

ansible_ssh_pass: MyPwd

Here, ansible_ssh_pass will be used for both the servers mentioned above. As we have the same variable for both servers, we are using group_vars. If we have a different password for the servers, then we create host_vars folder.

  • For variables different as per the target host (like target server IP addresses), create host_vars folder and create one file per alias with yml extension by using full colon.
  • For same variables on multiple hosts (like same password for diff servers), create group_vars folder and create one file with the group name in inventory and add the variables there.

Include statement in playbook

Not only variables, you can also segregate all your tasks into different files and call them in the playbook as needed. For this, create a folder called tasks in the same folder where you have your playbook file. In the tasks folder, create individual files as per your task. For example, one task file for installing a web server and other task file for starting the web service and so on.. Finally use the syntax,

- include tasks/filename.yml

When creating files in tasks folder, you can use any filename with yml extension. This is because we have to use the filename when using include statement.

Sample code

1
2
3
4
5
- name: Simple task create folder and write hostname
  hosts: centosVMs
  tasks:
    - include: tasks/createdirectory.yml
    - include: tasks/writehostname.yml
Info
In ansible, syntax of the same command will be different when used in different places in the script. For example, If you are using a variable name in a separate file, you must use single quotes. If you are referencing a variable name in between a command, then single quotes are not needed. Similarly, if you are using ansible_host variable in inventory file, it will be ansible_host = server01. If you are using the same variable in host_vars folder, it will be ansible_host: server01, as this will be in yaml format.

Asynchronous tasks and polling

From the official site:

By default Ansible runs tasks synchronously, holding the connection to the remote node open until the action is completed. This means within a playbook, each task blocks the next task by default, meaning subsequent tasks will not run until the current task completes. This behavior can create issues. For example, a task may take longer to complete than the SSH session allows for, causing a timeout. Or you may want a long-running process to execute in the background while you perform other tasks concurrently. Asynchronous mode lets you control how long-running tasks execute.

What if the task finishes quickly or fails immediately? We dont want to wait till the whole aysnc time is completed. For this, we can then use -p or poll command and specify the time in seconds so that Ansible checks the status of the operation at that time interval. If you are running Ansible command, use -B parameter to specify the timeout for the long running activity. If you want to use async timeout in playbook, use async: timeout value.

Tip
Ansible polls every 10 seconds by default.

ansible all -B 3600 -P 120 -a “/usr/bin/long_running_operation”

Here, the long running operation can run for 3600 seconds. Using the -p parameter we’ve specified the poll value to 2 minutes. So, this task can run for one hour and ansible checks the status of the script every 2 minutes.

To check the output, register the task to a variable. Using the async_status command you can then capture the output of the asynchronous task.

Even when using async or -B parameter, we are still using synchronous mode. To make it asynchronous, set the poll value to 0. This means ansible will start the first task and immediately move to the next task in the playbook or in the ansible command line.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
- name: Run an async task
  ansible.builtin.yum:
    name: docker-io
    state: present
  async: 1000
  poll: 0
  register: yum_sleeper

- name: Check on an async task
  async_status:
    jid: "{{ yum_sleeper.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 100
  delay: 10

Strategy

If you have 10 tasks to be executed on 5 servers, first task is executed on all 5 servers and second task is executed on all the 5 servers and so on.. This the default behavior and is called linear strategy.

If a server has different hardware as others and takes time to install software, you don’t want to wait other servers because of this. In this case, you want all tasks to run on all servers simultaneously. If one server takes long time, only that server will finish the tasks after long time. Other servers are not affected. To achieve this, add the below line to the playbook.

strategy: free

You can select a different strategy for each play as shown above, or set your preferred strategy globally in ansible.cfg, under the defaults section:

1
2
[defaults]
strategy = free

If you want only few servers to be worked upon, then you should use serial keyword. If you have 5 servers, and want only 3 servers to be processed at a time, add below code to the playbook. This is based on the above linear strategy.

serial: 3

Out of 5 servers, 3 servers will be processed first and the remaining servers next. You can also specify % of servers for the serial, like 30% etc.. so that out of all the servers 30% of the servers will be processed and then next 30% and so on..

By default Ansible works with 5 servers from your hosts list. If you want more severs to be worked upon simultaneously, you have to explicitly specify it. If you have the processing power available and want to use more forks, you can set the number in ansible.cfg:

1
2
[defaults]
forks = 30

or pass it on the command line: ansible-playbook -f 30 my_playbook.yml.

Controlling failed tasks

When you run multiple tasks on multiple servers, first task is executed on all the servers and second task is executed on all servers and so on.. This is the default behavior in Ansible. If one of the task fails on one of the servers, task execution will be stopped on that server alone and execution will continue on the other servers as they did not throw any errors.

If you want to change this behavior to stop the task execution on all the servers when one server failed, add below code to your playbook.

any_errors_fatal: true

If there is a task where you want to perform something, but the execution result is not 100% success. For example, you may want to email a team after all the tasks are completed. But for whatever reason if that fails, the whole execution will be stopped and marked as failure. To skip this, add below code to the task.

ignore_errors: yes

You can also write the execution log to a log file and look for the word “ERROR” or “FAILED” etc… and stop the execution when it finds those words.

Lookups

If you have data in a file separately you can refer it in your playbook using the lookup module. For more information on lookups checkout ansible documentation here.

Vault

If you want to encrypt any ansible data like password files or group_vars or host_vars variables etc… you can do it by using ansible_vault command. Once you enter the below command, you will be prompted to enter a password. This will be your vault password. Your file will be encrypted.

ansible_vault encrypt inventory.txt

When you want to use this inventory file, you have to use -ask-vault-pass parameter in the ansible command as shown below.

ansible-playbook myplaybook.yml -i serverinventory.txt -ask-vault-pass

You can also add the same vault in a file and use that file in the ansible-playbook command.

ansible-playbook myplaybook.yml -i serverinventory.txt -vault-password-file ./password.txt

You can encrypt the password in the password.txt file using any other programming language and use it in the above command.

ansible-playbook myplaybook.yml -i serverinventory.txt -vault-password-file ./decryptvaultpwd.py

In the above command, decrypt vault password python script will be executed to get the vault password and use it to decrypt the inventory file.

To view the contents of encrypted vault file, use ansible-vault view filename.txt and to create an encrypted vault file, use ansible-vault create filename.txt command.

Templates and filters

From the official site:

Ansible uses Jinja2 templating to enable dynamic expressions and access to variables and facts. You can use templating with the template module. For example, you can create a template for a configuration file, then deploy that configuration file to multiple environments and supply the correct data (IP address, hostname, version) for each environment. You can also use templating in playbooks directly, by templating task names and more. You can use all the standard filters and tests included in Jinja2. Ansible includes additional specialized filters for selecting and transforming data, tests for evaluating template expressions, and Lookup plugins for retrieving data from external sources such as files, APIs, and databases for use in templating.

In your playbook file define all your variables under vars: command and use it in the playbook where needed.

1
2
3
4
5
6
7
- name: Test Template
  hosts: server01
  vars:
    Name: linux
  tasks:
    - debug:
        msg: " The name is {{ Name }} "

These double curly brackets are called Jinja2 formatting. This is a generic template engine. This is not an native feature of Ansible.

In this Jinja2 templating, you can use filters like below

1
2
3
4
5
name is {{ Name }}
name is {{ Name | upper }}
name is {{ Name | lower }}
name is {{ Name | title }}
name is {{ Name | replace ("linux", "LinuxServer" }}

You can look at filters here: Using filters to manipulate data — Ansible Documentation

Ansible GUI

For managing Asible through GUI, you have tools like Red Hat Ansible Tower (Not free, paid software) and AWX (Free, opensource).

Conclusion

In this article, we’ve explained what is Ansible and its basic usage. We’ve also demonstrated examples for all the topics that are discussed in the article. We hope you have learned something new in this article.

All other DevOps categories are listed here: DevOps Tools. Have a look at the posts as per your topic of interest. Please feel free to share your thoughts about this article in the comments section below.

Your inbox needs more DevOps articles.

Subscribe to get our latest content by email.