Introduction to automated provisioning and deployment with Ansible

In this post, I am going to introduce one of the tools which we use here at Tratif for automating software installations.

Most of our projects written in Java require a lot of third party services to function (e.g. MySQL, RabbitMQ, Redis). This imposes a mandatory installation step to be performed before we deploy our Java services to a new machine. We call this step, as many of you probably do, provisioning.

Having provisioned a machine, we can deploy the layer with our software, so the solution is fully operational and the machine can be sent to the client.

Sounds like a simple two-step process – provision and deploy…

But, how do you actually go about provisioning and deployment? How do you handle dozens of installations that vary slightly or significantly? Finally, how do you track the differences between installations so that you know what is deployed where?

The last question is especially important when you are operating in an environment, where frequent deployments are not allowed. Although I wish all of you to live happily in Continuous Delivery world, it is not always possible.

Let us see how we can tackle some of these problems with Ansible.

What is Ansible?

Ansible is a tool that provides an automation language to engineers for managing IT infrastructure. It is very flexible and lightweight. It supports great variety of operating systems, has a rich library of modules and is fully customizable. Its agent-less nature means that you do not have to install anything on the target server. All you need to provide is SSH access to it. With that in place Ansible is able to connect to the server and perform tasks specified in provisioning and deployment scripts.

There are several basic concepts which will be covered in this article:

  • inventories and hosts
  • playbooks
  • variables
  • vaults
This article does not cover the installation process. To install Ansible please refer to the instructions under this link: http://docs.ansible.com/ansible/latest/intro_installation.html

Inventories and hosts

An inventory file defines a specific installation and hosts on which that installation should be performed. A host is a concrete machine. Inventory file allows you to define the actual host machines info by using details such as: IP address, SSH connectivity details, etc. It also gives you a way of assigning an inventory to a group. Groups will be described later.

Here is an example inventory file with two host machines – multi-node-installation:

[multi-node-installation]
app-server ansible_ssh_host=192.168.0.2 ansible_ssh_port=22 ansible_ssh_user=user ansible_ssh_private_key_file=~/.ssh/key.pem
db-server ansible_ssh_host=192.168.0.3 ansible_ssh_port=22 ansible_ssh_user=root

[app-host]
app-server

[db-host]
db-server

[app-hosts:children]
app-host

[db-hosts:children]
db-host

...

The above file defines an inventory named multi-node-installation which consists of two host machines named: app-server and db-server. Those hosts are assigned to two groups, respectively: app-hosts and db-hosts.

The :children suffix states that all variables defined for a group should be used by Ansible at runtime.

Playbooks

Automation can be thought of as a type of orchestration of tasks.

Playbook is a term used mostly in theatres, but it suits automation tasks quite well. A playbook is a specification of tasks which need to be played on a specific stage, i.e. a host machine.

Let us see an example playbook – play.yml:

- hosts: app-hosts
  become: yes
  tasks:
    - name: creating user my_user
      user:
        name: my_user
        state: present
    - name: creating group my_group
      user:
        name: my_group
        state: present
    - name: creating directory for my_service and its configuration
      file:
        path: /opt/my_service/configuration
        state: directory
        owner: my_user
        group: my_group
    - name: creating a configuration file for my_service
      copy:
        content: "{{ config_content }}"
        dest: /opt/my_service/configuration/my_service.conf
        owner: my_user
        group: my_group

- hosts: db-hosts
  become: yes
  tasks:
    - name: installing database
      yum:
        name: mysql-server
        state: latest

Here is the explanation of what each of the lines mean:

Line 1 – specifies all inventories and hosts on which this part of playbook is played – in our example, this will be executed on app-server machine only as per the setup in inventory file

Line 2 – specifies that all the tasks shall be ran as root user

Line 3 – specifies a start of a section with list of tasks to be acted out

Lines 4 to 23 – show a list of four tasks

Lines 5, 9, 13 and 19 – show name of module executed for a task. Below these lines are lines with parameters to the modules

Line 20 – shows a way of accessing a variable config_content (you will find more about variables later on) using a templating engine named jinja supported by Ansible

So, how do you actually play this playbook using Ansible? Here is how:

$ ansible-playbook play.yml -i multi-node-installation

The -i parameter specifies the inventory on which play.yml should be executed.

And here is an excerpt from running the playbook:

___________________
 PLAY [app-hosts] 
-------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

________________________
 TASK [Gathering Facts] 
------------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

ok: [app-server]
 ______________________________
  TASK [creating user my_user] 
 ------------------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

changed: [app-server]
 ________________________________
  TASK [creating group my_group] 
 --------------------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

changed: [app-server]
 __________________________________________
  TASK [creating directory for my_service] 
 ------------------------------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

changed: [app-server]
 _____________________________________________________
  TASK [creating a configuration file for my_service] 
 -----------------------------------------------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

changed: [app-server]
 _________________
  PLAY [db-hosts] 
 -----------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

____________________________
 TASK [installing database] 
----------------------------
\  ^__^
 \ (oo)\_______
   (__)\       )\/\
       ||----w |
       ||     || 

changed: [app-server]
 ____________
  PLAY RECAP 
 ------------
 \  ^__^
  \ (oo)\_______
    (__)\       )\/\
        ||----w |
        ||     ||

app-server : ok=7 changed=0 unreachable=0 failed=0

Ansible outputs the tasks that it is running. It displays a line with status of a task: ok, changed (failed and reachable). At the end, there is a play recap with summed up statuses of all tasks.

An important feature of Ansible is tasks idempotency. In case the installation fails for example during fifth task, you can re-run the playbook (after fixes) and Ansible will notice that the results of the first four tasks are already on the host, so it will not re-apply them.

Variables

Ansible allows you to define multitude of variables which can be used to determine properties such us software versions, configuration parameters, toggle switches, etc. You can define anything and the types of variables supported are extensive – string, boolean, list, dictionary. You can even define a multi-line string directly in a variable and later on use that content to fill a file on a machine.

Variable files allow you to configure a deployment for given environment requirements.

Let us see an example vars file:

my_service_version: '1.0.0'

is_ssl_enabled: yes
is_database_encrypted: False

feature_toggles: ['auto-save', 'newsletter', ...]

my_service_config:
  jar_service_name: 'my_service'
  http_proxy:
    port: 9050

ssl_certificate: |
  -----BEGIN CERTIFICATE-----
  AXfHRTSFjGdRTfgghj...

The syntax used for variable files is YAML.

Managing sensitive data – Ansible Vault

Deployment-specific data should be kept in variables, so that your scripts can be re-used for multiple installations (e.g. for test and prod environments). This often includes sensitive information, such as passwords. Typically, provisioning and deployment scripts are kept in a version control system such as Git, though. You probably do not want to give access to all passwords to everybody who has access to the repository. How can we store these variables in a safe way then?

Ansible allows you to encrypt files using its vault feature. Put all passwords, private SSH keys and other values that need to be securely stored in a text file. Then, use Ansible’s vault tool to encrypt and decrypt it.

You can define a vault password file that Ansible can use to encrypt and decrypt your vaults or provide a password via prompt each time. They can be passed directly between your team members.

Example vault_password file:

this is a vault password

Keep the above file secure!

Using Ansible’s vault feature with vault password file:

// encrypting a vault
$ ansible-vault encrypt vault_file --vault-password-file=./vault_password

// decrypting a vault
$ ansible-vault decrypt vault_file --vault-password-file=./vault_password

// editing a vault - this one allows you to make changes in a cmdln text editor like VI and upon closing the file Ansible will encrypt it back
$ ansible-vault edit vault_file --vault-password-file=./vault_password

Encrypted files will look similarly to the following example:

$ cat vault_file

 $ANSIBLE_VAULT;1.1;AES256
 63316233393232383033613231336535343662636538656234626165636461363135313638643231
 343665333064336164383...

One downside of vaults is that every time you encrypt it, even without any changes to the content, the ciphered content will change. This causes a VCS system to assume the file needs to be committed. It is sometimes burdensome when you want to quickly check a variable that happens to be in a vault file. Our practice is: decrypt the vault file, grab the variable value, and ask VCS to re-load the file from repository.

Example project structure

All the above files and concepts form a “provisioning and deployment automation” project, which can be stored in any VCS.

Let us see an example with nicely cut responsibilities:

provision/
  play.yml           - playbook for provisioning installation step
deploy/
  play.yml           - playbook for deploy installation step
inventories/         - folder with inventories data
  group_vars/        - a folder holding all group variables
    app-hosts/
      vars           - variables file for app-hosts
      vault          - vault file for app-hosts
    db-hosts/        - another group folder; you do not need to have a vault file everywhere
      vars
  multi-node-installation  - the inventory file

With the above structure, you can run the two steps:

$ ansible-playbook provision/play.yml -i inventories/multi-node-installation --vault-password-file=./vault_password
...
$ ansible-playbook deploy/play.yml -i inventories/multi-node-installation --vault-password-file=./vault_password

The division of provisioning and deployment steps is introduced not only to clearly cut responsibilities. It also gives an additional benefit which is being able to update only our software components on a machine which has been provisioned in the past. Typically, changes are done less often to the third party services than to internal components (just think how many times you change the database in your module as opposed to how often you introduce a new feature). This approach is a good practice as it saves a lot of time when executing the scripts.

Summary

As you saw, Ansible is a pretty handy tool. It allows you to define inventories, group them, specify variables for each group, encrypt files with sensitive data, define playbooks, and most importantly – run everything with a single command line.

There is a lot more to cover, so stay tuned for new posts on automating installations!

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *