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