Ansible

This section aims to be a guide of good practices defined by the ArCo architecture for those projects that need Ansible to configure the servers after being provisioned with Terragrunt/Terraform. The purpose of applying these practices is to have a common base for all projects inside the organization so the learning curve is smaller for those changing between different projects using this tool.

We have used to best practices Ansible page as a reference for most of what we are applying.

Structure

ArCo keeps almost the same resource organization recommended by Ansible. But we add a new level to group the inventory and playbooks configuration.

Any project using Ansible should have the following structure:

inventory/
    production                # inventory file for production servers
    staging                   # inventory file for staging environment

    group_vars/
        group1.yml             # here we assign variables to particular groups
        group2.yml
        group3/                # when we have sensible variables, we create a folder for particular groups
            vars.yml           # here we add every no sensible variables and the sensible variables do reference to vaults file
            vaults.yml         # here we add sensible variables with prefix vault_
    host_vars/
        hostname1.yml          # here we assign variables to particular systems
        hostname2.yml

library/                  # if any custom modules, put them here (optional)
module_utils/             # if any custom module_utils to support modules, put them here (optional)
filter_plugins/           # if any custom filter plugins, put them here (optional)

playbooks/
    site.yml                  # master playbook
    webservers.yml            # playbook for webserver tier
    dbservers.yml             # playbook for dbserver tier

    roles/
        common/               # this hierarchy represents a "role"
            tasks/            #
                main.yml      #  <-- tasks file can include smaller files if warranted
            handlers/         #
                main.yml      #  <-- handlers file
            templates/        #  <-- files for use with the template resource
                ntp.conf.j2   #  <------- templates end in .j2
            files/            #
                bar.txt       #  <-- files for use with the copy resource
                foo.sh        #  <-- script files for use with the script resource
            vars/             #
                main.yml      #  <-- variables associated with this role
            defaults/         #
                main.yml      #  <-- default lower priority variables for this role
            meta/             #
                main.yml      #  <-- role dependencies
            library/          # roles can also include custom modules
            module_utils/     # roles can also include custom module_utils
            lookup_plugins/   # or other types of plugins, like lookup in this case

        webtier/              # same kind of structure as "common" was above, done for the webtier role
        monitoring/           # ""
        fooapp/               # ""

Variables naming

In Ansible we have two different types of variables: globals and locals to the role. Each of them has a different naming convention. Local variables always have a prefix that allows to identify the role that variable belongs to. That prefix can be the role name itself or an acronym that identifies that role with no mistakes. On the other hand, global variables don’t have that kind of prefix.

Thanks to this we can take a big Ansible project and identify easily what variables are used for multiple roles and which are local to one single role.

# playbooks/nginx/vars/main.yml
nginx_path: "{{ apps_path }}/nginx"

# inventory/group_vars/all.yml
apps_path: /opt/apps

# inventory/group_vars/nginx.yml
nginx_path: /var/lib/nginx

In this example we can see how the rol ngins uses the global variable app_path. But in our particular case we want to use a different path and override the specific variable of the module. So we need to redefine the variable nginx_path with the new path.

Sensitive information

Sometimes we use files and variables that hold sensitive information like passwords or private keys. In these cases it is mandatory to encrypt their content before the code is submitted to the repository. This way if anyone gets access to the repository or its information won’t be able to read it if they don’t have privileges to do it.

Ansible provides a tool called Ansible Vault to encrypt that information in our Ansible scripts. The default algorithm it uses is AES and the encrypted output looks like this:

the_secret: !vault |
      $ANSIBLE_VAULT;1.1;AES256
      62313365396662343061393464336163383764373764613633653634306231386433626436623361
      6134333665353966363534333632666535333761666131620a663537646436643839616531643561
      63396265333966386166373632626539326166353965363262633030333630313338646335303630
      3438626666666137650a353638643435666633633964366338633066623234616432373231333331
      6564

To organize the sensitive variables in any project, the ArCo recommendation is to split the variables in two files: vaults.yml and vars.yml. The first defines the variable that really contains the sensitive information. These variables' names have to start with the prefix "vault_", so we can easily spot them. The later defines the variables we are actually using: both non sensitive variables and sensitive variables that reference the variables in vaults.yml.

inventory/
    group_vars/
        group3/
            vars.yml
            vaults.yml
# inventory/group_vars/group3/vars.yml
mysql_password: "{{ vault_mysql_password }}"

# inventory/group_vars/group3/vaults.yml
vault_mysql_password: !vault |
      $ANSIBLE_VAULT;1.1;AES256:dev
      62313365396662343061393464336163383764373764613633653634306231386433626436623361
      6134333665353966363534333632666535333761666131620a663537646436643839616531643561
      63396265333966386166373632626539326166353965363262633030333630313338646335303630
      3438626666666137650a353638643435666633633964366338633066623234616432373231333331
      6564

Encrypt a property

For encrypting variables you have to execute the next command:

ansible-vault encrypt_string --vault-id @prompt 'value-to-encrypt' --name 'variable_name'

ansible-vault command asks the vault key when @prompt is used. The result is the encrypt variable. You should modify the uncrypted variable with that value.

  variable_name: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          39343766306430653934316363306262303439313535666338313436613437393063306166333965
          3932303865623065653237363864363065336639653463360a613430383532363161363137386234
          39356239363862663566626334336666623161616662653162353064323863623664633431326361
          6365613762373735660a653937333135333836366234386561313337333064373663633161323832
          64396364623963323934393366323838303738643134373334336264646231373538
  Encryption successful

If you want to use a vault file to encrypt the variable, you could to use the next command:

ansible-vault encrypt_string --vault-id 'vault_file_path' 'value-to-encrypt' --name 'variable_name'

It´s possible to set multiple vault passwords to encrypt different files or variables. The value for --vault-id can specify the type of vault id (propmt, a file path, etc..) and a label for the vault-id ('sublive', 'prelive', 'live', etc..). This feature can be used starting from 2.4 Ansible´s version.

For example, for the vault-id 'sublive'`:

ansible-vault encrypt_string `--vault-id` sublive@prompt 'value-to-encrypt' --name 'variable_name'

Encrypting a file

For encrypting files you have to execute the next command:

ansible-vault create --vault-id dev@prompt foo.yml

Vaults

When working with sensitive data, it will be different in each of the environments we have, e.g. sublive, prelive, live. Just like the actual values differ in all of them, the same should happen with the master password used to encrypt/decrypt the sensitive properties. That way if a password is compromised the other environments will remain safe.

When we encrypt a property or file using Ansible Vault, we can specify the name of the vault to which the resource will be added by using the --vault-id attribute. For example if we want to encrypt a property for the live environment we should execute this command:

ansible-vault encrypt_string `--vault-id` live@prompt 'value-to-encrypt' --name 'variable_name'

Once the property is encrypted we can find in the output the vault used in the encryption.

  variable_name: !vault |
          $ANSIBLE_VAULT;1.1;AES256:live  #<-- Last parameter shows the vault-id
          39343766306430653934316363306262303439313535666338313436613437393063306166333965
          3932303865623065653237363864363065336639653463360a613430383532363161363137386234
          39356239363862663566626334336666623161616662653162353064323863623664633431326361
          6365613762373735660a653937333135333836366234386561313337333064373663633161323832
          64396364623963323934393366323838303738643134373334336264646231373538
  Encryption successful
If no vault is specified, Ansible will use a default vault.

Full Parent Role and Child Role

Whenever a new role is created it should have all needed tasks for what it was created. Let’s say we want to a web server that means to create users, routes, install and configure the server, install the service and restart the server. All those tasks should be defined in our role as independent tasks and the main.yml should call all of them in the proper order. We shouldn’t create independent roles for each of those tasks.

playbooks/
    setup_nginx.yml                   # setup webserver playbook

    roles/
        nginx/
            tasks/
                users.yml             # task which create user and groups for nginx
                directories.yml       # task which create directories to install nginx
                install_nignx.yml     # task which install nginx
                install_service.yml   # task which install the service to start, stop, restart nginx
                restart_service.yml   # task which restart nginx
                main.yml
            handlers/
                main.yml
            templates/
                nginx.conf.j2
            vars/
                main.yml
            defaults/
                main.yml
# playbooks/roles/nginx/tasks/main.yml

- name: Create users and groups
  include: users.yml

- name: Change owner of config, log and data directories
  include: directories.yml

- name: Setup configuration
  include: install_nignx.yml

- name: Install service
  include: install_service.yml

- name: Restart service
  include: restart_service.yml

In case we need to invoke only a particular task of the role we have to create a child role that invokes the parent role file. E.g. if we need a role that only restart the server we must do as follows:

playbooks/
    roles/
        restart_nginx/
            tasks/
                main.yml
# playbooks/roles/restart_nginx/task/main.yml

- include_roles:
    name: "nginx"
    tasks: "restart_service"
This way variable, files and dependencies are in the parent role and if we need to modify something we only have to do it in a single place.

Access using the AWS Bastion

When a project is deployed in the cloud the way to access to the machines and configure them is a but different. Firstly we need to access a machine called bastion. From there we can access the desired machine as long as we have enough privileges for that. To allow this behaviour we have to add two files to our project: ansible.cfg and ssh.config.

ansible.cfg                 # file where you configure the SSH client
ssh.config                  # file where you have the configuration of how to connect to bastion and ansible machines
inventory/
playbooks/
    site.yml
    roles/
        webtier/
        monitoring/
        fooapp/
# ansible.cfg
[ssh_connection]
ssh_args = -o ControlPersist=15m -F ssh.config -q

# ssh.config

Host bastion
    User                   ec2-user
    HostName               100.99.88.77
    ProxyCommand           none
    IdentityFile           ~/.ssh/private_key
    BatchMode              yes
    PasswordAuthentication no
    StrictHostKeyChecking  no

Host *
    ServerAliveInterval    60
    TCPKeepAlive           yes
    ProxyCommand           ssh -i ~/.ssh/private_key -W %h:%p ec2-user@100.99.88.77
    ControlMaster          auto
    ControlPath            ~/.ssh/mux-%r@%h:%p
    ControlPersist         8h
    User                   ec2-user
    IdentityFile           ~/.ssh/private_key
    StrictHostKeyChecking  no

Besides that we also need to add to the folder ` ~/.ssh` the file with the name we specified in the IdentityFile property with the private key to access the machines of that environment.

Usage

The first step to use Ansible is to install it in our machine following this guide.

Before executing any Ansible playbook we have to configure our local environment. For that we will add the private key needed to access the machines we are about to configure to the path ~/.ssh.

If our playbook isn’t using Ansible Vault to manage sensitive information we can execute it with this command:

ansible-playbook -i inventory/  playbooks/setup_cicd.yml

On the contrary, if we have properties encrypted with Ansible Vault we have to execute this other command:

ansible-playbook --vault-id vault-example@prompt -i inventory/  playbooks/setup_cicd.yml
Vault password (vault-example):

In this case Ansible will ask for the master password of the vault and will execute the playbook.