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 as:
server1.company.com
server2.company.com
or group them as:
[winservers]
server1.company.com
server2.company.com
[linuxservers]
linuxserver1.company.com
linuxserver2.company.com
[dbservers]
db1.company.com
db2.company.com
Or you can also use another format with an alias in the same line as:
dbserver ansible_host=db1.company.com
mailsrv ansible_host=mailsrv.company.com
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.
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:
-
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.
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.
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.
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:
- 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:
./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.
- name: Install webserver
hosts: linuxservers
roles:
- webserver
You can use this role in anywhere in the playbook.
Role example:
- 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 in calling your hosts in playbook file:
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.
server1 ansible_host=TargetServerIPaddress ansible_ssh_pass=PWD
server2 ansible_host=TargetServerIPaddress ansible_ssh_pass=PWD
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
- Arranging them in a group as per the server functionality.
If you are working with webservers, you can group them as:
[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.
Important point: In inventory file you use the format, ansible_host=IPAddress. But in the host_vars/alias.yml file, you must use the format as shown below. This is because this file is in yaml format.
ansible_host: IPAddress
Inventory file:
[linuxservers]
linuxserver1 ansible_host=192.168.1.1
linuxserver2 ansible_host=192.168.1.2
host_vars folder:
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:
[linuxservers]
linuxserver1 ansible_host=192.168.1.1
linuxserver2 ansible_host=192.168.1.2
group_vars file:
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
- name: Simple task create folder and write hostname
hosts: centosVMs
tasks:
- include: tasks/createdirectory.yml
- include: tasks/writehostname.yml
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.
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.
- 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:
[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
:
[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.
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
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