Cover Image

Introduction

Hey everyone, welcome back to the Learning Devops Series. For this “day” we will be looking at Ansible, an open-source automation platform used for provisioning, configuration management, and application deployment. Ansible is used pretty much everywhere you look now-a-days in the IT field as it is so multifaceted. You can use ansible to install software on servers, setup user access permissions, and even setup your development environment on your local machines. If you are going into this field then you are going to at least need to know about Ansible as it will make your day-to-day workings a lot easier.

This post will cover a basic introduction into Ansible and a few of the add ons that come with it. Also, near the end of the post, we will be testing the knowledge we’ve learned by putting it to use in a lab. This lab will integrate our Ansible playbooks into a Terraform script that will deploy some Ubuntu Server VMs on Azure. If you haven’t read my previous post on Terraform, I recommend you do so to get you up to speed.

Ansible

As already stated previously, Ansible is an open-source automation platform used for provisioning, configuration management, and application deployment. The automation is done using a few files: the inventory file and what are known as “playbooks”.

Inventory file

The inventory file plays a crutial role in the automation of systems as it tells ansible where to apply the playbooks and allow grouping of systems into there own blocks. Inventory files can come in two flavors: static inventory files or dynamic inventory files. The dynamic inventory file is use when you will not know what the IP address or the domain name of the server will be when you apply the playbooks. This is useful for IaC testing setups as you more than likely won’t know what the public IP address will be when you run the code. We’ll take a more in-depth look at dynamic inventory files later in the post.

The other type of inventory files is the static inventory file. This is pretty much what you think it is. The inventory file contains the IP address or domain name of the servers you know and already have access to. An example of the static inventory file is shown below:

inventory

[webserver]
wb1 ansible_host=192.168.0.34 ansible_user=wbuser
wb2 ansible_host=webserver2.example.com ansible_user=mario
wb3 ansible_host=192.168.0.36 ansible_user=peach

[database]
db1 ansible_host=192.168.0.60 
db2 ansible_host=192.168.0.65
db3 ansible_host=192.168.0.67

[prod]
wb1
db2

[qa]
wb2
db3

[testing]
wb3
db1

In this file, you can see it is written in the TOML format and it has been grouped in a few different ways. The first is the inventory file has grouped 3 webservers and 3 database servers under their respective blocks. This allows for a clean and easy to read inventory. Next under both the webserver and database groups, you have an alias of the servers. These basically allow the user to logically name servers and to place them under other blocks like the prod, qa or testing blocks. After that, there is the ansible_host option which is assigned the actual IP address or even the domain name of those servers. Finally, under the webserver block, the ansible_user option assigns the user for the specific server which ansible should connect to.

Playbooks

Next up are playbook files. These playbooks are written in YAML, an easy to read format used in a ton of automation software. An example of YAML is shown below:

playbook.yml

---
- hosts: webserver
  become: true

  tasks:
  - name: install latest version of Nginx
    apt:
     name:
       - nginx
    state: latest
  - name: start nginx service
    service:
      name: nginx
      state: started

In this example you can see that YAML uses dashes and spaces to indicate the different hierarchies of the code. To break this playbook down a bit more, lets start at the top. The hosts property tells ansible on which hosts to run this playbook on. In this case, webserver means only the webserver hosts defined in the inventory file. The become property tells ansible to become another user. With it being set to true, ansible will try to become root. The tasks property is where we starting defining the tasks that need to get completed in the playbook. In this example there are two tasks that need to get completed.

The first is the task labeled “install latest version of Nginx” using the name property. This uses the apt property to install a list of packages using the apt package manager. In this case it is the nginx package, displayed in a YAML list. The state property tells ansible to install the latest version of the package.

The second task “start nginx service” does exactly that. Using the service property, the nginx package’s state is set to started.

Running the playbook

To run the playbook above, you can do so like:

ansible-playbook -i inventory playbook.yml

This will then produce output like below:

PLAY [webserver] ***************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************
ok: [wb2]
ok: [wb1]

TASK [Install latest version of Nginx] *****************************************************************************************************
ok: [wb2]
ok: [wb1]

TASK [start nginx service] *****************************************************************************************************************
ok: [wb1]
ok: [wb2]

PLAY RECAP *********************************************************************************************************************************
wb1                        : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
wb2                        : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

As you can see, when running the playbook ansible gathers the information about what servers it should look at, then it moves on to the first task on installing Nginx and then finishes off with starting the Nginx service. The PLAY RECAP shows a listing of all of the items that have succeeded, changed items, unreachable servers, and if any of the tasks failed, got skipped, rescued or ignored.

Roles

Now that we’ve written and run our first playbook, what if I told you that there is a better way to write them? This new way of writing playbooks are called roles. Roles allow the author of the playbooks to write them in such a way as to decrease the amount of repetition normally found in stand alone playbooks. The structure of a role is as such:

myplaybook
|_ inventory
|_ playbook.yml
|_ roles
   |_ nginx
      |_ tasks
         |_ main.yml

As you can see, the main ansible items are still present but the task of installing and starting nginx have been decouple from the main playbook and placed in its own file called main.yml. This makes the role reusable and more consise. The code inside the main.yml file is as such:

- name: install and check nginx latest version
  apt:
    name:
      - nginx
    state: latest
- name: start nginx
  service:
    name: nginx
    state: started

The playbook also has to be rewritten to be able to use the role. The playbook should look like this:

---
- hosts: webserver
  roles:
    - nginx

Ansible Vault

Now that we have inventory files, playbooks, and roles covered, lets look at Ansible Vault. Ansible Vault is used to encrypt sensive data like passwords and API keys so that they are unreadable to unwanted eyes. This is useful for when you need to have a playbook in a public git repository and you need to have the sensitive data in order for the playbook to work.

To see Ansible Vault in action, lets create another role for mysql. Create the directores mysql/tasks. The directory structure should look like this:

myplaybook
|_ inventory
|_ playbook.yml
|_ roles
   |_ nginx
   |_ mysql
      |_ tasks
         |_ main.yml

Next, in the roles/mysql/tasks/main.yml write this:

---
- name: Update apt cache
  apt: update_cache=yes cache_valid_time=3600
- name: Install required software
  apt: name="{{ packages }}" state=present
  vars:
    packages:
    - python3-mysqldb
    - mysql-server 
- name: Create mysql user
  mysql_user: 
    name={{ mysql_user }} 
    password={{ mysql_password }} 
    priv=*.*:ALL
    state=present

What this playbook will do is update the apt repository, install the specified packages for mysql to work, then create a mysql user with a password. Anything within the double curly braces {{ }} are variables within ansible. You can see there is one for packages and the mysql_user and mysql_password. As we want to keep the username and the password private, we then need to create a new directory at the root of our project called group_vars. Inside group_vars we need to create a directory that matches a block within our inventory file so that ansible knows where the variables belong. In this project the directory would be called database. Inside the database directory, create another main.yml. Inside the main.yml you can then place the variables we will be using for the database setup:

---
mysql_user: mysql_username
mysql_password: SuP3rSecurePassw0rd

To encrypt the mysql username and password, you just need to run:

ansible-vault encrypt group_vars/database/main.yml

NOTE: If you ever need to decrypt this file for whatever reason, you can run: ansible-vault decrypt group_vars/database/main.yml

Ansible Vault will then prompt you for a password, just enter something memorable and then take a peak at the group_vars/database/main.yml file again. The contents should look like this:

$ANSIBLE_VAULT;1.1;AES256
36396265323436373135393534646666373166346462633736323066646133653634396364666337
6432666633333662366236343762343363316436613738610a646234616465386666623433376362
31376565666336323332306536306631616663376331626239633864363533663762323661633862
3534623063613166330a306364626464396639613138613662366464663533623663613732646533
35343435393738306135303036393436326436316361656463366239643133376238393961323631
33313565656430623861333664323066333035303631373532383364336665396237633139306131
663266363537343430346166643664326438

Pretty neat right! Now before you try to run your playbook, just note that ansible itself won’t be able to read the group_vars/database/main.yml without the password you entered. To be able to run your playbook, you can run:

ansible-playbook -i inventory playbook.yml --ask-vault-pass

However, if you are trying to add your playbook to a CI/CD pipeline, it wouldn’t be able to function. To solve this, you can place the password you used to encrypt group_vars/database/main.yml in a vault_pass.txt file. Then just run:

ansible-playbook -i inventory playbook.yml --vault-password-file vault_pass.txt

Just make sure to place the vault_pass.txt file into your .gitignore file before pushing this project to Github or the like.

Lab

Now that we’ve covered everything you need to know about how to start using ansible, let’s try this in a lab. To start, go to this link to my repostory on the matter. Then pull the repo down using git and go into the ch3 directory:

git clone https://github.com/weiseguy1/learning-devops.git
cd ch3

This directory will be full of a lot of files that we’ve just talked about along with some Terraform files and an inventory-gen.sh bash script. Also, within this lab is the README.md which has all of the instructions you will need to get it to work.

If you remember at the beginning of the post I talked about how there are two different types of inventory files. Well, the inventory-gen.sh helps to create a dynamic inventory file. Let’s look at it:

#!/bin/sh

# DESCRIPTION: Automatically generate inventory file for ansible

database=$(az vm list --resource-group rg-ansible-vm-lab --show-details --query "[*].{TAGS:tags.role, PIP:publicIps}" -o tsv | grep database | awk '{print $2}')

webserver=$(az vm list --resource-group rg-ansible-vm-lab --show-details --query "[*].{TAGS:tags.role, PIP:publicIps}" -o tsv | grep webserver | awk '{print $2}')
echo "[databases] $database " | tr ' ' '\n' >> inventory
echo "[webservers] $webserver" | tr ' ' '\n' >> inventory

This script uses the az cli command to find the Public IP adddress information based on the tags, then place the output into a newly created inventory file. This way is not the only way, of generating dynamic inventory files. In the book, it talks about using the Azure collection available through Ansible Galaxy. However, at the time of this writing, the Azure collection has some issues when generating a dynamic inventory file.

Wrapping it up

All and all, I think this chapter on ansible was pretty interesting as it showed me how to setup systems differently. What is nice about ansible is that once you have the playbooks written, you basically never have to do that process manually ever again. The only thing that I did have an issue on was the part on Azure Collections, which I spent literal days trying to get working to no avail. That is the reason I created the AZ CLI script which works without a hitch. Also, I really wanted to start using AZ CLI as I’m starting to study for that AZ-104 and I thought it would be good to get more experience with that tool as well. With that said, I plan to start writing a lot more ansible playbooks in the future as it is such a handy tool to have in the Systems Administrator toolbelt.

Next up in the series, I’ll be going over Packer and how to generate easily deployable images which you can use in your own Azure infrastructure. Stay tuned and thanks for reading!