1. Overview
The proposed architecture aims to provision a system by following the steps below:
Create ansible’s directory hierarchy, playbooks, roles, taskfiles, etc. (basically,the logic that will be used to provision the system), and maintain it under version control.
ssh into the master node and download the repo from version control. Consider using read-only deploy keys to download the repo without having to type an username and password; especially in unattended deployments.
Run a one-shot script to perform an initial setup of the environment required for ansible to run properly during and after deployment, followed by the actual software execution (see Section 3).
Check the bootstrap log and verify there were no errors and/or unexpected behaviors.
If errors arose, fix them and re-run the bootstrap script.
2. Provisioning logic
2.1. Directory hierarchy
The logic should consider constant creation of new playbooks, roles, taskfiles, etc., allowing to easy scale in the future. Take the example below:
# tree /etc/ansible . ├── ansible.cfg ├── environments │ ├── dev │ │ ├── group_vars │ │ └── inventory │ └── prod │ ├── group_vars │ └── inventory ├── playbooks │ ├── playbook_1.yml │ ├── playbook_2.yml │ └── playbook_n.yml ├── roles │ ├── role_1 │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ └── vars │ │ └── main.yml │ ├── role_2 │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ └── vars │ │ └── main.yml │ └── role_n │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ ├── templates │ └── vars │ └── main.yml ├── scripts │ ├── bootstrap.sh │ └── vault-client.sh └── site.yml
It is designed so that upon calling site.yml tasks from a particular set of playbooks,
from the playbooks folder, are applied. This behavior can be accomplished by
manually importing the playbooks or using variables:
Importing playbooks |
Using variables |
# /etc/ansible/site.yml
---
- import_playbook: playbooks/playbook_1.yml
- import_playbook: playbooks/playbook_2.yml
- import_playbook: playbooks/playbook_n.yml
|
# /etc/ansible/environments/prod/group_vars/group1
---
playbook: playbook_1
# /etc/ansible/environments/prod/group_vars/group2
---
playbook: playbook_2
# /etc/ansible/environments/prod/group_vars/groupN
---
playbook: playbook_n
# /etc/ansible/site.yml
---
- import_playbook: "playbooks/{{ playbook }}.yml"
|
One could couple all playbooks inside site.yml, but that would make future scalability difficult
and potentially cause problems if a large number of people is working on the same project
(take git merge conflicts for example).
If one-shot playbooks (playbooks that run only once, such as whose involving firmware updates) are to be managed,
it is recommended to modify the directory hierarchy so that the playbooks folder holds
them. For example:
.
└── playbooks
├── auto
│ ├── playbook_1
│ ├── playbook_2
│ └── playbook_n
└── manual
├── playbook_1
├── playbook_2
└── playbook_n
which would be run like:
Note
More options can be used, but they will mostly depend on the playbook’s functionality.
ansible-playbook -i /path/to/inventory \
/path/to/ansible/playbooks/manual/<playbook>
2.2. Ansible launcher
Using multiple environments, launching ansible from a non-standard location, among others may result in a large inconvinient command. Furthermore, if a run is to be triggered by an external entity, such as a script that requires ansible to run certain tasks within particular servers (see Section 1.3), additional concerns arise (e.g. What if I run ansible while it is already running? how to control the number of runs at a given time?).
To solve the abovementioned issues one can create a template ansible will render as a script:
1#!/bin/bash
2# Title : run_ansible
3# Description : Run ansible
4# Author : Tomas Felipe Llano Rios
5# Date : Nov 21, 2018
6# Usage : bash run_ansible [options]
7# Help : bash run_ansible -h
8#==============================================================================
9
10# tee only reads and prints from and to a file descriptor,
11# so we need to use two execs to read and print from and to
12# both stdout and stderr.
13#
14# Receives stdout, logs it and prints to stdout
15exec > >(tee -ia /{{ ansible_log_dir }}/scheduled_run.log)
16# Receive stderr, log it and print to stderr.
17exec 2> >(tee -ia /{{ ansible_log_dir }}/scheduled_run.log >&2)
18
19function log {
20 echo "[$(date --rfc-3339=seconds)]: $*"
21}
22
23function print_help {
24 echo -e "\nUsage: run_ansible [options]\n"
25 echo -e "Where [options] include all ansible-playbook options,"
26 echo -e "except for --vault-id and --inventory-file.\n"
27 echo -e "Installed using the following configuration:"
28 echo -e "\tEnvironment: $env"
29 echo -e "\tAnsible home: $repo_dir"
30 echo -e "\tAnsible config file: $cfg_file"
31 echo -e "\tInventory file: $inv_file\n"
32
33 command -v ansible-playbook > /dev/null 2>&1
34 if [ "$?" -eq 0 ]; then
35 echo -e "ansible-playbook options:\n"
36 ansible-playbook -h | awk '/Options:/{y=1;next}y'
37 else
38 echo -e "See ansible-playbook help for more information.\n"
39 fi
40}
41
42# Always release lock before exiting
43function finish {
44 flock -u 3
45 rm -rf $lock
46}
47trap finish EXIT
48
49# Create lock to prevent the script from being
50# executed more than once at a given time.
51declare -r script_name=`basename $0`
52declare -r lock="/var/run/${script_name}"
53if [ -f "$lock" ]; then
54 echo "Another process (pid:`cat $lock`) is already running"
55 trap - EXIT
56 exit 1
57fi
58exec 3>$lock
59flock -n 3
60log "Lock acquired"
61declare -r pid="$$"
62echo "$pid" 1>&3
63
64declare -r env={{ env }}
65declare -r repo_dir={{ repo_dir }}
66declare -r cfg_file="$repo_dir/ansible.cfg"
67declare -r inv_file="$repo_dir/environments/$env/inventory"
68
69if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
70 print_help
71 exit 0
72fi
73
74log "Updating repository"
75cd $repo_dir
76git pull
77cd -
78
79log "Executing ansible"
80
81export ANSIBLE_CONFIG="$cfg_file"
82export DEFAULT_ROLES_PATH="$repo_dir/roles"
83export ANSIBLE_EXTRA_VARS="env=$env repo_dir=$repo_dir $ANSIBLE_EXTRA_VARS"
84ansible-playbook --inventory-file "$inv_file" \
85 --extra-vars "$ANSIBLE_EXTRA_VARS" \
86 --vault-id "$env@$repo_dir/scripts/vault-secrets-client.sh" \
87 $repo_dir/site.yml \
88 -vv \
89 $@
90unset ANSIBLE_CONFIG
In order to allow for easy migration of the script to, for example, another folder while still pointing
to the project sources the template obtains one variable, ansible_log_dir, from group_vars
and the remaining two from the bootstrap script. This is corroborated
in line 47 from Section 3, where there are two extra vars (env, repo_dir)
passed to ansible-playbook; all of which are dynamically discovered just before the first run.
On subsequent runs, run_ansible will keep passing down these values recursively. One could argue it is better
to just place the task inside a one-shot playbook; this implies, however, that modifications made to the template
should be applied manually and if the rendered script is changed it would not be restored automatically.
2.3. Scheduled run
It would be tedious to manually run ansible every time a change is done to the project. A nice approach to schedule when one wants provisioning to occur is creating a task to install a cron managing the time at which to call ansible:
Hint
Given the limited environment in which crons are run, one may need to add a task such as:
- name: Adding PATH variable to cronfile
cron:
name: PATH
user: root
env: yes
value: /bin:/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
cron_file: ansible_scheduled_run
or, if your launcher is written in bash, make it act as if it had been invoked as a login shell
by using the -l option (#!/bin/bash -l).
- name: Schedule ansible to run every 30 minutes
cron:
name: "run ansible every 30 minutes"
user: root
minute: "*/30"
job: "/path/to/run_ansible"
cron_file: ansible_scheduled_run
3. bootstrap
By means of a few initial instructions the script should install ansible in the target system, prepare the environment it requires and trigger its first run. To further review whether the bootstrap failed or succeeded, and take apropriate actions, it is strongly recommended to log the output. Take the following program for example:
1#!/bin/bash
2# Title : bootstrap.sh
3# Description : Install and configure ansible
4# Author : Tomas Felipe Llano Rios
5# Date : Nov 21, 2018
6# Usage : bash bootstrap.sh <environment>
7#==============================================================================
8
9# tee only reads and prints from and to a file descriptor,
10# so we need to use two execs to read and print from and to
11# both stdout and stderr.
12#
13# Receive stdout, log it and print to stdout.
14exec > >(tee -ia ./bootstrap_run.log)
15# Receive stderr, log it and print to stderr.
16exec 2> >(tee -ia ./bootstrap_run.log >&2)
17
18declare -r env="$1"
19declare -r script_path="$(readlink -e $0)"
20declare -r script_dir="$(dirname $script_path)"
21declare -r repo_dir="${script_dir%/*}"
22declare -r cfg_file="$repo_dir/ansible.cfg"
23declare -r inv_file="$repo_dir/environments/$env/inventory"
24
25# Check if the environment provided exists within ansible's
26# directory hierarchy.
27declare -r envs="$(ls $repo_dir/environments/)"
28if [ "$envs" != *"$env"* ]; then
29 echo -e "\nUnrecognized environment. Choose from:\n$envs\n"
30 exit 1
31fi
32
33# ansible-vault requires pycrypto 2.6, which is not installed by default
34# on RHEL6 based systems.
35declare -i centos_version=`rpm --query centos-release | awk -F'-' '{print $3}'`
36if [ "$centos_version" -eq "6" ]; then
37 /usr/bin/yum --enablerepo=epel -y install python-crypto2.6
38fi
39# Install ansible.
40/usr/bin/yum --enablerepo=epel -y install ansible
41
42# Run ansible.
43export ANSIBLE_CONFIG="$cfg_file"
44export DEFAULT_ROLES_PATH="$repo_dir/roles"
45ansible-playbook \
46 --inventory-file="$inv_file" \
47 --extra-vars "env=$env repo_dir=$repo_dir" \
48 --vault-id "$env@$repo_dir/scripts/vault-secrets-client.sh" \
49 $repo_dir/site.yml \
50 -vv
After running the script, there should be a cronfile in /etc/cron.d and a rendered version of the run_ansible script.
4. Example
Create directory tree
cd /some/dir/ mkdir -p ansible cd ansible && git init git remote add origin <uri> mkdir -p {playbooks,environments,roles,scripts} mkdir -p roles/master/{tasks,templates} mkdir -p environments/production/group_vars/ # Create the appropriate files according to your needs. # A good start would be: #touch site.yml \ # Calls the master.yml playbook # playbooks/master.yml \ # Calls the master role # roles/master/tasks/main.yml \ # Renders template # roles/master/templates/run_ansible.j2 # environment/production/inventory # environment/production/group_vars/all
Download repo
Note
Consider using read-only deploy keys to download the repo without having to type an username and password; especially in unattended deployments.
ssh <user>@<server> cd /usr/local/ git clone <uri>
Bootstrap. Suppose you run the bootstrap script from
/usr/local/ansible/scripts/, which discovers and passes two variables to ansible: env and repo_dir:1declare -r env="$1" 2declare -r script_path="$(readlink -e $0)" 3declare -r script_dir="$(dirname $script_path)" 4declare -r repo_dir="${script_dir%/*}" 5declare -r cfg_file="$repo_dir/ansible.cfg" 6declare -r inv_file="$repo_dir/environments/$env/inventory" 7ansible-playbook \ 8 --inventory-file="$inv_file" \ 9 --extra-vars "env=$env repo_dir=$repo_dir" \ 10 --vault-id "$env@$repo_dir/scripts/vault-secrets-client.sh" \ 11 $repo_dir/site.yml \ 12 -vv
Executing the script in a production environment, like
bootstrap.sh prod, will cause variables to be passed to ansible asenv=productionandrepo_dir=/usr/local/ansible/; therefore producing arun_ansiblescript pointing to/usr/local/ansible/.Check for errors
less bootstrap_run.log