A simple-as-possible tutorial for using Ansible
This site provides a simple introduction to Ansible, for those unfamiliar with or new to it.
The instructions below are intentionally as simple and prescriptive as possible, without covering the wealth of options and configurability Ansible allows. For those interested in further options, follow the provided links to the in-depth Ansible guides for those concepts.
Why Ansible?
And perhaps most importantly, the “code” (3) is YAML – simple, structured text documentation that’s also executable.
An Ansible inventory is a simple text file that details the various servers onto which to deploy.
The inventory file can be defined in various formats and can even be dynamically-constructed, eg. from a cloud service.
Perhaps the simplest (and most common) form of an inventory is ini-style:
[group]
hostname
group
, in square-brackets.group
names themselves are arbitrary, and specific only to the playbooks you are deploying.For example:
[balancer]
load.somewhere.com
[appserver]
app.somewhere.com
[database]
db.somewhere.com
This inventory defines three groups: balancer, appserver, and database. Each group has a single, distinct host.
The inventory also defines any connectivity details for a given host:
[balancer]
load.somewhere.com ansible_user='abc' ansible_ssh_private_key_file='~/.ssh/id_abc'
This inventory connects to load.somewhere.com using SSH, as user
abc
, and using a private key in~/.ssh/id_abc
.
[appserver]
app.somewhere.com ansible_user='administrator' ansible_password='MyPassw0rd' ansible_port=5986 ansible_connection=winrm ansible_winrm_server_cert_validation=ignore
This inventory connects to app.somewhere.com using WinRM (Windows Remote Management), as user
administrator
, using passwordMyPassw0rd
, through port5986
, and ignores SSL certificate validation (ie. accepts a self-signed certificate from the host).
The Ansible documentation provides a full set of variables that can be used to configure connectivity.
Before we get to playbooks, it is worth understanding one of the basic modularisation mechanisms of Ansible: the role.
Roles provide a way to package together all of the tasks needed to deploy a particular “thing” in a re-usable way, for others to make use of simply by referring to the role within their playbook. As such, roles abstract away the need to know all of the underlying task-by-task details of deployment of that “thing”.
For example, you may have one role that deploys a particular piece of database software like bernardovale.db2
and another that deploys a load balancer nginxinc.nginx
.
While roles themselves are implemented through YAML
, and therefore are typically easy to read and understand, they are generally intended to be used as-is (without modification). Any inputs that may vary in your particular environment will typically be catered for through variables.
Ansible provides a package-manager-of-sorts for obtaining roles called Galaxy, so a role can be easily installed using a command like:
$ ansible-galaxy install nginxinc.nginx
An Ansible playbook contains the instructions for deploying something.
Playbooks typically:
Playbooks are written in YAML
, and generally follow a simple structure defining these points:
---
- name: Deploy DB2 to database hosts
hosts:
- database
roles:
- bernardovale.db2
- name: Deploy NGINX to load-balancer hosts
hosts:
- balancer
roles:
- nginxinc.nginx
This example playbook would first ensure that DB2 is deployed to all hosts under the
[database]
group of the inventory, and then ensure that NGINX is deployed to all hosts under the[balancer]
group of the inventory. The actual set of tasks needed for each of these deployments is simply pulled-in from thebernardovale.db2
role and thenginxinc.nginx
role, respectively.
Playbooks can be run through the ansible-playbook
command, passing the inventory and playbook name:
$ ansible-playbook -i inventory_filename playbook.yml
As the playbook runs, it will print out information about what task it is carrying out, against which server. A green “ok” indicates that the task has completed without making any change, a yellow “changed” indicates that the task has completed and made some change to the system, and a red “failed” indicates that there was an error. (By default, any host that has a “failed” task will no longer have any tasks run against it for the remainder of the playbook.)
A summary, by host, of the count of “ok”, “changed”, and “failed” tasks will also be printed at the end of the playbook’s run.
As mentioned above, variables are generally used by both roles and playbooks to provide parameterisation to the uniqueness of your particular environment.
Common examples include things like directory locations, enabling or disabling optional features, and user credentials.
It is considered good practice for roles to document the minimal set of variables they expect, as well as any further (optional) ones that may influence how the role deploys its “thing”. When this is not done, typically the variables can be found inside a role by looking at that roles’ defaults/main.yml
file.
There are many ways to pass variables through to Ansible, and if you decide to opt for using many approaches at the same time it is important to understand their precedence. If you are creating your own variables, make sure you also avoid the use of reserved words.
However, for simplicity, often the easiest way to make use of variables in a consistent way is through one (or both) of the following two methods. These will then be automatically picked up each time when running the playbook.
Create a sub-directory (relative to your playbook) called group_vars
, in which you place a single file called all.yml
. This single file can then contain all of the variables you want to configure, across all of the groups involved in your playbook.
---
db2_binary:
url: https://mycompany.com/downloads/db2_10_5.tar.gz
location: /ansible/files/db2_10.5.tar.gz
dest: /tmp
nginx_type: opensource
nginx_install_from: nginx_repository
In this example, the location of DB2 binaries and some NGINX options are defined, both taken from the documentation of these actual roles.
If you want to override a particular variable for a single host, often this is most easily done through a host_vars
sub-directory (relative to your playbook), in which you place a file for each server for which you want to override variables. (The filename should be <hostname>.yml
.)
---
nginx_unit_enable: true
In this example, the
nginx_unit_enable
has been switched on for only one particular host (controlled by the name of the file).
A common concern when deploying is ensuring that sensitive information (like user credentials) is appropriately protected.
Ansible addresses this concern through its vault capability. The vault allows you to encrypt the contents of a file defining such sensitive variables, so that using them requires a password to decrypt them.
A common pattern to make use of a vault is to replace any sensitive information in your simple variable files (eg. group_vars/all.yml
) with other variables. For example:
---
db2_instance_owner: ""
db2_instance_owner_pwd: ""
Then create a separate file that contains only the sensitive information (eg. group_vars/vault.yml
):
---
vault_db2_instance_owner: db2inst1
vault_db2_instance_owner_pwd: myDb2Inst1Passw0rd
And encrypt this separate file using Ansible vault:
$ ansible-vault encrypt group_vars/vault.yml
Ensure that your playbook includes this vault as well, by simply adding a vars_files
line:
---
- name: Deploy DB2 to database hosts
hosts:
- database
roles:
- bernardovale.db2
vars_files:
- vault.yml
And then when you run your playbook, simply ensure it asks for the vault password:
$ ansible-playbook -i inventory_filename playbook.yml --ask-vault-pass
I would generally suggest always running playbooks (eg. on your control machine) through GNU screen
.
This will ensure that even if you are disconnected for some reason from your control machine (ie. network problems), your playbook will nonetheless continue to run. Furthermore, you’ll even be able to recover the output of it running by simply re-attaching to the virtual TTY of screen
itself.