Modern times

Modern times

The ansible is a fictional device invented by Ursula K. Le Guin. The ansible allows fast communication between two points across the space, regardless of the distance.

It doesn’t surprise that the Red Hat’s ansible shares the same name as it gives a simple and efficient way to manage servers and automate tasks, yet remaining very lightweight with minimal requirements on the target machines.

The winning points of ansible are simplicity and idempotency allowing the devops engineer to setup the deployment within minutes using just a collection of yaml files.

Ansible is normally included in any major distribution and installs within the python virtual environment.

In this first tutorial we’ll see how to configure three linux servers with the PostgreSQL binaries installed from the apt pgdg repository. The playbooks are designed to work with any debian based distribution available in the pgdg repository and more. In particular we’ll use Devuan ASCII machines for our examples.

For ansible’s full documentation please visit docs.ansible.com

Configuring the environment

This working example is available at the branch 01_install of the git repository https://github.com/the4thdoctor/dynamic_duo. The repository provides an hosts.example file in the directory inventory. To make the tutorial work we need to copy hosts.example into a new file hosts which shall be eventually adapted to the specific users’s configuration (e.g. different ip addresses).

git clone https://github.com/the4thdoctor/dynamic_duo.git
cd dynamic_duo
git checkout 01_install
cd inventory
cp hosts.example hosts

A quick look at the hosts file shows just three servers listed under the hosts group, db01,db02,backupsrv. For each server we have a variable srv_ip which defines the server’s ip address. This variable is useful if we don’t have the DNS for resolving the names.

There is only an additional group apt which lists the hosts group as a children. This group will be used for installing the required packages on our servers.

[hosts]
backupsrv srv_ip=192.168.1.22
db01 srv_ip=192.168.1.23
db02 srv_ip=192.168.1.24



[apt:children]
hosts

Installing ansible

We’ll use ansible 2.5 in a python 3 virtual environment. For the install steps please watch the following asciicast.

As our machines are not yet configured with the public key we need the program sshpass installed on the machine running ansible.

Please refer to your distribution for the instructions on how to install it.

When ansible and sshpass are installed from the git repository’s root we can run a simple test in order to check that ansible is working correctly.

ansible --user ansible --ask-pass --become-method=su --ask-become-pass  -m ping hosts
SSH password:
SU password[defaults to SSH password]:
db02 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
backupsrv | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
db01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

We are explicitly telling ansible to ask the ssh password with --ask-pass and to ask for the root user password with --ask-become-pass. We are also changing the become method with --become-method=su because our devuan servers don’t have sudo installed yet.

The -m ping is a module which emulates the ping command and tests if we can correctly escalate the privileges. We are telling to run the module against the inventory’s group hosts.

Ansible setup…with ansible!

Now we are ready to configure ansible for the further deployments. This, of course, is just an example that can be adapted to suit the reader’s security needs. Check the configuration file ansible.cfg for the various options.

The playbook initial_configuration.yml will ship our local public key to the ansible user, enabling the passwordless login. The playbook also installs sudo and configures the ansible user to perform a privilege escalation without providing the user’s password.

Before running the tasks the vars_prompt: section will ask for the public key’s location. If omitted it will default in ~/.ssh/id_rsa.pub.

---
- hosts: hosts
  remote_user: ansible
  become: yes
  become_method: su
  vars_prompt:
    - name: "public_key"
      prompt: "Enter your public key location"
      default: "~/.ssh/id_rsa.pub"
  tasks:
    - name: add the public key to the ansible user
      authorized_key:
        user: ansible
        state: present
        key: "{{ lookup('file', public_key) }}"
    - name: install sudo
      apt:
        name: sudo
        update_cache: yes
        cache_valid_time: 86400
    - name: Allow the ansible user to have passwordless sudo
      lineinfile:
        dest: /etc/sudoers
        state: present
        regexp: '^ansible'
        line: 'ansible ALL=(ALL) NOPASSWD: ALL'
        validate: 'visudo -cf %s'

The authorized_key task using the lookup file plugin reads the key contents and stores them in the ansible user’s authorized_key file.

The second task using the apt module installs sudo.

The third task using the module lineinfile adds the line to file /etc/sudoers which allow the ansible user to escalate the privileges without password. The file modification is validated using visudo in order to avoid a broken environment.

The following asciicast shows how to run the playbook. Differently from the first test we just need to use the options --ask-pass --ask-become-pass. The become method is already defined within the playbook with the key become_method: su.

Now we should be able login with the ansible to any of our servers using our ssh key and we should be able to escalate the privileges using sudo without being asked for a password.

The setup playbook

Now lets have a look inside the folder inventory/group_vars. The file apt is used to store the variables we want to use on all servers included in the group apt.

This file consists of a dictionary with a codename translation, the dictionary pggd_codename. This is necessary to translate the devuan ascii codename to debian stretch.

With the list pgsql_versions we specify the PostgreSQL major versions we want to install. Another list pgsql_packages stores the version specific postgresql packages without the major version. The third list common_packages is used for listing the common packages we want to install first.

pgdg_codename:
  ascii: stretch

pgsql_versions:
  - 9.6
  - 10


pgsql_packages:
- postgresql-
- postgresql-client-
- postgresql-contrib-


common_packages:
  - postgresql-client-common
  - postgresql-common
  - pgbackrest
  - python-psycopg2

The setup.yml playbook is quite minimal. We define the host group where to run the tasks and the roles we want to run within the playbook.

---
- hosts: apt
  roles:
    - hosts
    - apt
  remote_user: ansible
  become: yes

Lets have a look to the two roles hosts and apt.

The role hosts

This role consists of one single tasks which ships a template hosts file to the servers.

- name: Ship the hostname template with the server names
  template:
    src: hosts.j2
    dest: /etc/hosts
    owner: root
    group: root
    mode: 0644
  when: no_dns | default(False)

The when: conditional is skipped by default. In order to enable the host configuration we shall pass to ansible-playbook the variable no_dns=True. In that case the hosts file will be replaced by a version with the hostnames pointing to the ip addresses assigned to the variable srv_ip in the inventory’s hosts group.

The hosts file is generated using the jinja2 template hosts.j2.

127.0.0.1	localhost
127.0.1.1	{{ ansible_hostname }}

{% for host in groups['hosts'] %}
{% if ansible_hostname!=hostvars[host]['inventory_hostname'] %}
{{ hostvars[host]['srv_ip'] }}	{{ hostvars[host]['inventory_hostname'] }}
{% endif %}
{% endfor %}

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

The template configures the ansible_hostname to be resolved into the localhost address. Then a for loop over the servers listed in the group hosts configures each server hostname to be resolved to the corresponding ip. This is made using the key inventory_hostname within the fact hostvars and the server ip stored in the hostvars srv_ip .

We skip the local host, which is already configured, with an if construct.

The role apt

The role apt consists of five tasks.

---
- name: Set the codename to the pgdg_codename translation or the lsb_release, whether it comes first
  set_fact:
    codename: "{{ pgdg_codename[ansible_lsb['codename']] | default(ansible_lsb['codename'])  }}"

- name: Add the postgresql key to the apt repository
  apt_key: url="https://www.postgresql.org/media/keys/ACCC4CF8.asc" state=present

- name: add postgresql repository
  apt_repository:
    repo: "deb http://apt.postgresql.org/pub/repos/apt/ {{ codename }}-pgdg main"
    state: present

- name: install common packages
  apt:
    name: "{{ item }}"
    update_cache: yes
    cache_valid_time: 86400
  with_items: "{{ common_packages }}"


- name: install postgresql versioned packages
  apt:
    name: "{{ item.0 }}{{ item.1 }}"
    update_cache: yes
    cache_valid_time: 86400
  with_nested:
    - "{{ pgsql_packages }}"
    - "{{ pgsql_versions }}"

We set the a fact with the codename determined by the lsb_release fact. However the assignment is first attempted using the ansible_vars['codename'] as a key of the dictionary pgdg_codename set in group_vars/apt. If the key is missing then the default filter returns the value of ansible_vars['codename']. This allow the playbook to manage the codenames of custom distributions like Devuan ASCII.

The two subsequent tasks are quite straightforward. The postgresql signing key is added to the apt keyring, then the repository for the distribution’s codename is added. For the complete details please check the apt postgresql wiki page.

The last two tasks install the common packages and then the versioned packages.

This separation may seem useless now, but will become useful when the playbook will be extended, in the future, with the PostgreSQL configuration.

The following asciicast shows the playbook run.

As a result we’ll find the default main clusters created during the first install of the packages.

ansible@db01:~$ pg_lsclusters
Ver Cluster Port Status Owner    Data directory               Log file
9.6 main    5432 online postgres /var/lib/postgresql/9.6/main /var/log/postgresql/postgresql-9.6-main.log
10  main    5433 online postgres /var/lib/postgresql/10/main  /var/log/postgresql/postgresql-10-main.log

The rollback playbook

In order to reset the machines to the empty state the repository provides a rollback.yml playbook. This playbook by default does nothing as we won’t want to risk data loss if executed by accident.

In order to enable the rollback is necessary to pass an extra variable to the playbook. This is the full list of variables.

ansible-playbook rollback.yml --extra-vars="rbk_apt=True"

Three variables are allowed for this rollback.

  • rbk_hosts=True removes the hosts configuration
  • rbk_apt=True removes the apt configuration
  • rbk_all=True removes all the changes applied

rollback hosts

The playbook consists of just one task which replaces the hosts file on the servers, restoring the the default configuration where the server name is resolved to 127.0.0.1.

rollback apt

This playbook restores the machine to the previous state before the PostgreSQL install.

In particular,

  • The main clusters created automatically by the debian packages are deleted
  • All the PostgreSQL packages and relative configurations are removed with purge
  • The pgdg apt signing key is removed
  • The source file pointing to the pgdg apt repository is deleted

Wrap up

Ansible is a simple and powerful ally for automating complex tasks.

This simple example shows how to setup ansible and configure three empty machines with the PostgreSQL apt repository, supporting multiple PostgreSQL major versions.

In the next post we’ll add a new ansible role which will configure in a simple way the ssh login across the servers for the postgres OS user.

Thanks for reading.

Modern times By Taste of Cinema [Public domain], via Wikimedia Commons