Intro To Ansible#
Ansible is an open-source configuration management, provisioning and application deployment platform written in Python and using YAML (YAML Ain't Markup Language) as a configuration language.
Ansible makes its connections from your computer to the target machine using SSH.
The one server site requirement is an SSH server. General familiarity with SSH is desirable if you're using Ansible -- as well as being a baseline skill for server administration.
Installation#
Ansible is installed on the orchestrating computer -- typically your desktop or laptop. It is a large Python application (though a fraction the size of Plone!) that needs specific Python packages from the Python Package Index (PyPI).
That makes Ansible a strong candidate for a Python virtualenv installation If you don't have virtualenv installed on your computer, do it now.
virtualenv may be installed via an OS package manager, or on a Linux or BSD machine with the command:
sudo easy_install-2.7 virtualenv
Once you've got virtualenv, use it to create a working directory containing a virtual Python:
virtualenv ansible_work
Then, install Ansible there:
cd ansible_work
bin/pip install ansible
Now, to use Ansible, activate that Python environment.
source bin/activate
ansible
Note
Trainers: check to make sure everyone understands the basic source activate
mechanism.
Now, let's get a copy of the Plone Ansible Playbook.
Make sure you're logged in to your ansible_work
directory.
Unless you're participating in the development of the playbook, or need a particular fix, you'll want to check out the STABLE
branch.
The STABLE
branch is a pointer to the last release of the playbook.
git clone -b STABLE --single-branch https://github.com/plone/ansible-playbook.git
Or,
git clone https://github.com/plone/ansible-playbook.git
cd ansible-playbook
git checkout STABLE
That gives you the Plone Ansible Playbook. You'll also need to install a few Ansible roles. Roles are Ansible playbooks packaged for distribution. You may pick up everything with a single command.
cd ansible-playbook
ansible-galaxy install -p roles -r requirements.yml
If you forget that command, it's in the short README.rst file in the playbook.
Note
The rationale for checking the Plone Ansible Playbook out inside the virtualenv directory is that it ties the two together. Months from now, you'll know that you can use the playbook with the Python and Ansible packages in the virtualenv directory.
We check out the playbook as a subdirectory of the virtualenv directory so that we can search our playbooks and roles without having to search the whole virtualenv set of packages.
Ansible Basics#
Connecting To Remote Machines#
To use Ansible to provision a remote server, we have two requirements:
We must be able to connect to the remote machine using ssh; and,
We must be able to issue commands on the remote server as root (superuser) via sudo.
You'll need to familiarize yourself with how to fulfill these requirements on the cloud/virtual environment of your choice. Examples:
Using Vagrant/VirtualBox
You will initially be able to log in as the "vagrant" user using a private key that's in a file created by Vagrant. The user "vagrant" may issue sudo commands with no additional password.
Using Linode
You'll set a root password when you create your new machine. If you're willing to use the root user directly, you will not need a sudo password.
When setting up a Digital Ocean machine
New machines are typically created with a root account that contains your ssh public key as an authorized key.
AWS
AWS EC2 instances are typically created with a an account named "root" or a short name for the OS, like "ubuntu", that contains your ssh public key as an authorized key. Passwordless sudo is pre-enabled for that account.
The most important thing is that you know your setup. Test that knowledge by trying an ssh login and issuing a superuser command.
ssh myuser@myhost.com # (what user/hostname did you use? are you asked a password?)
...
myhost.com $ sudo ls # (are you asked for your password?)
Inventories#
Ansible runs on a local computer, and it acts on one or more remote machines. We tell Ansible how to connect to remote machines by maintaining a text inventory file.
There is a sample inventory configuration file in your distribution. It's meant for use with a Vagrant-style VirtualBox.
cat vbox.cfg
myhost ansible_port=2222 ansible_host=127.0.0.1 ansible_user=vagrant ansible_private_key_file=~/.vagrant.d/insecure_private_key
This inventory file is complicated by the fact that a VirtualBox typically has no DNS host name and uses a non-standard port and a special SSH key file. Because of this we have to specify all those things.
If we were using a DNS-known hostname and our standard ssh key files, it could be much simpler:
direct.newhost.com ansible_ssh_user=root
Ansible inventory files may list multiple hosts and may have aliases for groups of hosts. See https://docs.ansible.com for details.
Playbooks#
We're going to cover just enough on Ansible playbooks to allow you to read and customize Plone's playbook. Ansible's documentation is excellent if you want to learn more.
In Ansible, an individual instruction for the setup of the remote server is called a _task_. Here's a task that makes sure a directory exists.
This uses the Ansible file
module to check to see if a directory exists with the designated mode.
If it doesn't, it's created.
Tasks may also have execution conditions expressed in Python syntax and may iterate over simple data structures.
In addition to tasks, Ansible's basic units are host and variable specifications.
An Ansible playbook is a specification of tasks that are executed for specified hosts and variables. All of these specifications are in YAML.
Quick Intro To YAML#
YAML isn't a markup language, and it isn't a programming language either. It's a data-specification notation like JSON.
Except that YAML -- very much unlike JSON -- is meant to be written and read by humans. The creators of YAML call it a "human friendly data serialization standard".
Note
YAML is actually a superset of JSON. Every JSON file is also a valid YAML file.
But if we fed JSON to the YAML parser, we'd be missing the point of YAML, which is human readability.
Basic types available in YAML include strings, booleans, floating-point numbers, integers, dates, times and date-times. Structured types are sequences (lists) and mappings (dictionaries).
Sequences are indicated by list-member lines with leading dashes:
- item one
- item two
- item three
Mappings are indicated with key/value pairs with colons separating keys and values:
one: item one
two: item two
three: item three
Complex data structures are designated with indentation:
# a mapping of sequences
american:
- Boston Red Sox
- Detroit Tigers
- New York Yankees
national:
- New York Mets
- Chicago Cubs
- Atlanta Braves
# a sequence of mappings
-
name: Mark McGwire
hr: 65
avg: 0.278
-
name: Sammy Sosa
hr: 63
avg: 0.288
Basic types read as you'd expect:
- one # string "one"
- 1 # integer 1
- 1.0 # float 1.0
- True # boolean True
- true # also boolean True
- yes # also boolean True
Finally, remember that this is a superset of JSON:
- {a: one, b: two} # mapping
- [one, two, three] # sequence
Want to turn YAML into Python data structures? Or Python into YAML?
Python has several YAML parser/generators. The most commonly used is PyYAML.
Quick code to read YAML from the standard input and turn it into pretty-printed Python data:
#! /usr/bin/python
import yaml
import pprint
import sys
pprint.pprint(yaml.load(sys.stdin.read()), indent=2)
Quick Intro To Jinja2#
YAML doesn't have any built-in way to read a variable. Ansible uses the Jinja2 templating language for this purpose.
A quick example: Let's say we have a variable timezone
containing the target server's desired timezone setting.
We can use that variable in a task via Jinja2's double-brace notation: {{ timezone }}
.
Jinja2 also supports limited Python expression syntax and can read object properties or mapping key/values with a dot notation:
{{ instance_config.plone_version < '5.0' }}
There are also various filters and tests available via a pipe notation.
For example, we use the default
filter to supply a default value if a variable is undefined.
- name: Set timezone variables
tags: timezone
copy: content={{ timezone|default("UTC\n") }}
dest=/etc/timezone
owner=root
group=root
mode=0644
backup=yes
Jinja2 also is used as a full templating language whenever we need to treat a text file as a template to fill in variable values or execute loops or branching logic.
Here's an example from the template used to construct a buildout.cfg:
zcml =
{% if instance_config.plone_zcml_slugs %}
{% for slug in instance_config.plone_zcml_slugs %}
{{ slug }}
{% endfor %}
{% endif %}
Playbook Structure#
An Ansible "play" is a mapping (or dictionary) with keys for hosts, variables and tasks. A playbook is a sequence of such dictionaries.
A simple playbook:
- hosts: all
vars:
... a dictionary of variables
tasks:
... a sequence of tasks
The value of hosts could be a single host name, the name of a group of hosts, or "all".
Variables#
Notifications And Handlers#
We may also specify "handlers" that are run if needed.
- hosts: all
vars:
... a dictionary of variables
tasks:
- name: Change webserver setup
...
notify: restart webserver
...
handlers:
- name: restart webserver
service: webserver
state: restarted
Handlers are run if a matching notification is registered. A particular handler is only run once, even if several notifications for it are registered.
Roles#
Ansible has various ways to include the contents of YAML files into your playbook. "Roles" do it in a more structured way -- much more like a package.
Roles contain their own variables, tasks and handlers. They inherit the global variable environment and you may pass particular variables when they are called.
Plone's Ansible Playbook includes several roles for chores such as setting up the load balancer and web server.
Other roles are fetched (the role source itself is fetched) by ansible-galaxy
when we use it to set up requirements.
Most are fetched from GitHub.
An simple Ansible playbook using roles:
- hosts: all
vars:
... a dictionary of variables
pre-tasks:
... tasks executed before roles are used.
roles:
... a sequence of role invocation mappings like:
- role: haproxy
var1: value1
var2: value2
when: install_loadbalancer|default(True)
...
tasks:
... other tasks, executed after the roles
handlers:
... handlers for our own tasks; roles usually have their own
If we want to pass variables to roles, we add their keys and values to the mapping.
Take a look at the when: install_loadbalancer|default(True)
line above.
A when
key in a role or task mapping sets a condition for execution.
For conditionals like when
, Ansible expects a Jinja2 expression.
We could also have expressed that when
condition as "{{ install_loadbalancer|default(True) }}"
.
Ansible interprets all literal strings as little Jinja2 templates.