Introduction
This guide will help you get started on using Ansible, an open-source tool you can use to automate and maintain the software and configurations of your Linux systems, as well as handle custom software deployments.
Ansible is programmed in Python, however you do not need to know Python to use it. You may need to gain an understanding of YAML, Jinja templates, and/or read the Ansible Project Documentation.
In this guide we use Ansible to maintain the state of your local development environment. After you are done you’ll be able to apply what you’ve learned beyond your local Linux environment.
To follow this guide, it is recommended that you setup an Amazon Cloud9
environment with the default Amazon Linux EC2 instance. The Cloud9 service is
free, as is the EC2 service to new Amazon AWS users for the first 12 months.
If you’ve used Amazon AWS before, then your cost should be less than $10 per
month as long as you are using a t2.micro
EC2 instance that is configured to
turn off after 30 minutes of non-activity.
If you do not have an environment setup yet, see the Amazon Cloud9 Environment Setup guide to get started.
This guide also assumes that you have experince with Git. This guide is oriented around the “infrastructure as code” methodology, where all our desired configurations are stored in a Git repository.
Establishing a Git repository
Create a new Github repository to store your Ansible configuration. After
you’ve done this, use git
to clone the repository into your local environment.
Here’s the command I used with my own repository:
git clone git@github.com:redconfetti/cloud9-dev.git
Installing the Ansible Engine
Amazon Linux includes Python 2.7 and 3.5 by default. Installing Ansible using the Yum package manager leads to conflicts, so use the Python 2.7 version of PIP to install Ansible.
# Install Ansible using Python 2.7
/usr/bin/pip-2.7 install ansible
If you’re using a different Linux distribution, see the Ansible Installation Guide for other system installation instructions.
Ansible Configuration
The default Ansible configuration file is located under
/etc/ansible/ansible.cfg
. Ansible will over-ride these default settings if
there are any found in a .ansible.cfg
file located in your home directory, or
an ansible.cfg
file located in the current directory.
Ansible is designed to be used from a specific machine, designated as the
“management node”, that is used to orchestrate the setup and configuration of
other machines. This is why the default configurations exist globally
under /etc/ansible
.
For our purposes we want Ansible to use the configuration we place in our
repository, so create a new ansible.cfg
file in the root folder of your
repository with the following contents
[defaults]
inventory = hosts.yml
This will help us to avoid needing to use the -i hosts.yml
parameter with
our Ansible commands, however it does introduce a possible security risk.
Since we’re running an EC2 instance that we’re not sharing with other users,
we’re not running the risk of another user modifying our ansible.cfg
file.
Ansible Configuration Documentation:
- Avoiding security risks with ansible.cfg in the current directory
- Configuring Ansible
- Ansible Configuration Settings
- ansible-config
Ansible Inventory
Ansible is able to manage the state of multiple machines. By default, the
inventory is defined in /etc/ansible/hosts
using a format similar to an
INI file. Alternatively you can use a YAML file to define your inventory.
As you saw above, we configured Ansible to recognize the hosts.yml
file
located in the root directory of the repository.
Create the hosts.yml
file with the following contents that defines the local
machine as ‘local’.
all:
hosts:
local:
ansible_host: localhost
ansible_connection: local
This configures a host named ‘local’ that represents the local EC2 instance that you’re working from when you use the command line interface (Terminal) in Cloud9.
For more information, see Ansible - Working with Inventory
Ansible Ad-Hoc Commands
Ansible is designed to connect to hosts configured in your inventory file using SSH key authentication. Because we’re configured to connect to our local system, we do not have to worry about SSH connections or configuration.
Ansible can be used to perform checks against hosts using a single command line command, known as an ‘ad-hoc’ command.
Run the command ansible all -m ping
and you should see the following.
$ ansible all -m ping
local | SUCCESS => {
"changed": false,
"ping": "pong"
}
If you see the output above indicating success, then you’re ready to move onto the next step.
For more information, see Introduction To Ad-Hoc Commands.
Ansible Playbooks
Ad-hoc commands are useful, but playbooks are where we can define multiple tasks that maintain the state of your system(s).
Much like the hosts.yml
file defined above, playbooks are defined in YAML
format. YAML is not a programming language, but a format for storing
information. In this case the information is our configuration that Ansible
modules are defined to interpret and apply to the hosts we specify.
A playbook can contain one or more “plays” that applies a certain state to a group of hosts.
The Amazon Linux distribution uses a package manager called Yum (Yellowdog Updater Modified). Ansible provides a Yum module which allows us to use this package manager to install and update software packages on the host.
Create a file named local.yml
with the following contents in your root
directory.
---
# This playbook deploys the entire setup to the Cloud9 development environment.
- hosts: local
tasks:
- name: upgrade all packages
become: yes
yum:
name: '*'
state: latest
Next, run the following command to run this playbook.
ansible-playbook local.yml
You should see output similar to this:
PLAY [local] *******************************************************************
TASK [Gathering Facts] *********************************************************
ok: [local]
TASK [upgrade all packages] ****************************************************
ok: [local]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
Now all of the software packages installed within your EC2 instance running Amazon Linux are updated.
See Intro to Playbooks or Modules by Category.
Ansible Modules
We just covered a task that used the Yum module to update all the installed software packages on the server.
Ansible supports many modules that serve many different purposes. You can view a list of all available modules by running:
ansible-doc -l
You can read the documentation for a specific module by using the command:
ansible-doc <module-name>
# View 'hostname' module documentation
ansible-doc hostname
You can also browse modules online starting with Modules by Category.
Ansible Templates
Most Unix/Linux utilities and daemons can be configured using plain text files. Ansible supports the ability to define configuration file templates that use the Jinja2 template syntax.
Let’s create a file called gitconfig.j2
to define our Git client
configuration.
# {{ ansible_managed }}
[core]
editor = /usr/bin/nano
[user]
name = {{git_client_name}}
email = {{git_client_name}}
Now update the local.yml
playbook file so that it includes the ‘vars’ that
define our git client name and email address. Also add the tasks to output the
value of the ansible_user_id
variable, and configure the .gitconfig
in that
users home directory.
---
# This playbook deploys the entire setup to the Cloud9 development environment.
- hosts: local
vars:
git_client_name: PeeWee Herman
git_client_email: peewee@example.com
tasks:
- name: upgrade all packages
yum:
name: '*'
state: latest
- debug: var=ansible_user_id
- name: configure git client
template:
src: gitconfig
dest: "/home/{{ ansible_user_id }}/.gitconfig"
owner: "{{ ansible_user_id }}"
group: "{{ ansible_user_id }}"
mode: 0644
As you can see, the ‘debug’ task allows us to see what value is registered
for the ansible_user_id
variable. This is useful for troubleshooting
your task configurations when they are failing.
$ ansible-playbook local.yml
PLAY [local] *******************************************************************
TASK [Gathering Facts] *********************************************************
ok: [local]
TASK [upgrade all packages] ****************************************************
ok: [local]
TASK [debug] *******************************************************************
ok: [local] => {
"ansible_user_id": "ec2-user"
}
TASK [configure git client] ****************************************************
changed: [local]
PLAY RECAP *********************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0
Now check the state of the Git configuration file.
$ cat ~/.gitconfig
# Ansible managed
[core]
editor = /usr/bin/nano
[user]
name = PeeWee Herman
email = peewee@example.com
The ‘Ansible managed’ comment at the top is simply a string that can be
configured in the ansible.cfg
to inform users that the configuration file
being viewed is configured by Ansible.
Ansible managed
is the default string for this variable. You can redefine
this string to include the user-id of the Ansible user as well as the date and
time, although this will result in Ansible reporting that the coniguration
file has been changed everytime you run the task.
See ansible_managed
ansible.cfg
[defaults]
inventory = hosts.yml
ansible_managed = Ansible managed, do not edit directly: last update by {uid} on %Y-%m-%d, %H:%M:%S
Let’s run the Playbook one more time.
$ ansible-playbook local.yml
PLAY [local] *******************************************************************
TASK [Gathering Facts] *********************************************************
ok: [local]
TASK [upgrade all packages] ****************************************************
ok: [local]
TASK [debug] *******************************************************************
ok: [local] => {
"ansible_user_id": "ec2-user"
}
TASK [configure git client] ****************************************************
changed: [local]
PLAY RECAP *********************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0
As you can see, it reports that it changed the configuration file. If we check the configuration file, you’ll see that it now includes the message with user, date, and time.
$ cat ~/.gitconfig
# Ansible managed, do not edit directly: last update by ec2-user on 2019-03-25, 06:28:58
[core]
editor = /usr/bin/nano
[user]
name = PeeWee Herman
email = peewee@example.com
Ansible Roles
If you placed all your tasks in a playbook, it could get very large and wouldn’t be as easy to manage. This is why Ansible supports grouping your automation components (variables, templates, tasks, etc) into re-usable “roles”.
It’s often that a role named ‘common’ is created to store settings that apply across all your hosts, such as updating the system software using Yum, setting the timezone, etc. Other roles are defined separate of the ‘common’ role that apply to certain nodes of your cluster or network, such as ‘webserver’ or ‘loadbalancer’. You can even get more detailed with roles named after the daemons you want running, such as ‘apache’ or ‘nginx’. It’s all up to you.
Taken from Ansible Docs - Roles
Roles expect files to be in certain directory names. Roles must include at least one of these directories, however it is perfectly fine to exclude any which are not being used. When in use, each directory must contain a
main.yml
file, which contains the relevant content:
tasks
- contains the main list of tasks to be executed by the role.handlers
- contains handlers, which may be used by this role or even anywhere outside this role.files
- contains files which can be deployed via this role.templates
- contains templates which can be deployed via this role.vars
- other variables for the role (see Ansible Docs - Using Variables for more information).defaults
- default variables for the role (see Ansible Docs - Using Variables for more information).meta
- defines some meta data for this role. See below for more details.
Let’s move our tasks to a new role called ‘development’.
To get started, establish a directory for your roles to contain the
development
role, containing a tasks
and templates
folder. Within tasks
create a new main.yml
file, and move the Git configuration template into the
templates
folder.
It should look like this when you’re done:
- roles
- development
- tasks
- main.yml
- templates
- gitconfig.j2
- tasks
- development
mkdir -p roles/development/tasks
mkdir -p roles/development/templates
touch roles/development/tasks/main.yml
mv gitconfig.j2 roles/development/templates
Next move your tasks from the local.yml
to roles/development/tasks/main.yml
.
Additionally, update the path for the gitconfig
template so that it is
reflected as templates/gitconfig
(you don’t need to include the .j2
file
extension).
---
- name: upgrade all packages
become: yes
yum:
name: '*'
state: latest
- debug: var=ansible_user_id
- name: configure git client
template:
src: templates/gitconfig
dest: "/home/{{ ansible_user_id }}/.gitconfig"
owner: "{{ ansible_user_id }}"
group: "{{ ansible_user_id }}"
mode: 0644
Now update local.yml
so that it no longer defines the tasks for the play, but
instead points to the development
role.
---
# This playbook deploys the entire setup to the Cloud9 development environment.
- hosts: local
vars:
git_client_name: PeeWee Herman
git_client_email: peewee@example.com
roles:
- development
If you run ansible-playbook local.yml
once again, the output should be the
same as before with no errors.
Variables
The Ansible engine makes it possible to re-use a role that you have defined on different hosts with differing configurations. This is made possible through the use of variables. Variables can be defined in your hosts file per each host.
Let’s move our Git name and email address from our local.yml
playbook, and place it within hosts.yml
so that it applies to our local
host.
Here we’ve defined a ‘development’ group, and have placed our ‘local’ host underneath that group. We’ve also defined the variables that should apply to all hosts within that ‘development’ group.
all:
children:
development:
hosts:
local:
ansible_host: localhost
ansible_connection: local
vars:
git_client_name: PeeWee Herman
git_client_email: peewee@example.com
If you want to inspect your inventory for errors, you can use the
ansible-inventory
command to inspect how Ansible is interpretting your
configuration.
$ ansible-inventory --list
{
"_meta": {
"hostvars": {
"local": {
"ansible_connection": "local",
"ansible_host": "localhost",
"git_client_email": "peewee@example.com",
"git_client_name": "PeeWee Herman",
"repository": "https://github.com/redconfetti/redconfetti.github.io",
}
}
},
"all": {
"children": [
"development",
"ungrouped"
]
},
"development": {
"hosts": [
"local"
]
},
"ungrouped": {}
}
$ ansible-inventory --graph
@all:
|--@development:
| |--local
|--@ungrouped:
The preferred way to store variables isn’t to define them in the hosts.yml
file though. Instead you can create a host_vars
directory, and then name
the files within it after each host. You can also create a group_vars
directory, and then name the files within it after each group.
Let’s create a host variable file for our ‘local’ host.
mkdir host_vars
touch host_vars/local.yml
Next let’s move our variables from the hosts.yml
into our
host_vars/local.yml
file.
host_vars/local.yml
git_client_name: PeeWee Herman
git_client_email: peewee@example.com
This should result in our hosts file being slim again.
hosts.yml
all:
children:
development:
hosts:
local:
ansible_host: localhost
ansible_connection: local
Facts
In addition to the variables that you define, you can also use a feature of Ansible known as “facts”. Facts are variables created by the Ansible engine that contain information about the host you are logging into… what user Ansible is acting as on the system, the command line environment, networking, etc.
To get a list of all the variables Ansible collects about our system run
the command: ansible local -m setup
.
Defaults
Roles are intended to be re-usable, but it could be tedious having to define variables for every single value that a task or template might need defined.
This is why roles support defaults defined in defaults/main.yml
.
To make our role more flexible, let’s define defaults for the name and email address used by the Git client.
mkdir -p roles/development/defaults
touch roles/development/defaults/main.yml
After you’ve done this, add the following to defaults/main.yml
.
git_client_name: "{{ ansible_user_gecos }}"
git_client_email: "{{ ansible_user_id }}@{{ ansible_fqdn }}"
To properly test this, go into host_vars/local.yml
and comment out the
git_client_name
and git_client_email
variables.
# git_client_name: PeeWee Herman
# git_client_email: peewee@example.com
Run the playbook once more and see what happens.
$ ansible-playbook local.yml
$ cat ~/.gitconfig
# Ansible managed, do not edit directly: last update by ec2-user on 2019-03-25, 19:42:28
[core]
editor = /usr/bin/nano
[user]
name = EC2 Default User
email = ec2-user@ip-172-31-18-245.us-west-2.compute.internal
As you can see Ansible provided our defaults in the configuration file. Now our role is more reusable.
For more information, see Ansible Docs - Using Variables.
Defining Variables in Files
It’s worth mentioning that you can also define variables in external files.
You more than likely have some passwords and API keys that you surely don’t want to check into your repository (even if it’s private).
To work around this, you can configure your playbook like the following.
---
# This playbook deploys the entire setup to the Cloud9 development environment.
- hosts: local
roles:
- development
vars_files:
- ~/.ansible_secrets.yml
tasks:
- debug: var=secret_password
- debug: var=some_api_key
Next create a ~/.ansible_secrets.yml
file with the following contents:
---
secret_password: abcdef123456
some_api_key: A65B90148F091E5F3F7E4DFFECD1B074
When you run your playbook, it should reflect the secrets you configured.
$ ansible-playbook local.yml
PLAY [local] *******************************************************************
TASK [Gathering Facts] *********************************************************
ok: [local]
TASK [debug] *******************************************************************
ok: [local] => {
"secret_password": "abcdef123456"
}
TASK [debug] *******************************************************************
ok: [local] => {
"some_api_key": "A65B90148F091E5F3F7E4DFFECD1B074"
}
PLAY RECAP *********************************************************************
local : ok=3 changed=0 unreachable=0 failed=0
Now you can define variables in a file that you want to keep secret, but not
check them into your repository. At most you might want to check in a file
named ansible_secrets.example.yml
that contains bogus passwords and keys,
and can be copied to ~/.ansible_secrets.yml
and then modified if needed
in the future.
A good practice is to add instructions to your README.md
file, like so:
# Setup
Run the following command to copy the template to your home directory
```shell
cp ansible_secrets.example.yml ~/.ansible_secrets.yml
```
Next, modify `~/.ansible_secrets.yml` to reflect the proper API keys
and passwords.
For more information, see Ansible Docs - Defining Variables In Files.
Ansible Vault
Instead of defining your variables in a file that you keep separate of your repository, there is an option to store your secrets in an encrypted file within your repository.
Ansible Vault provides support for many commands related to working with encrypted variable files.
You can run your playbook with a flag to let ansible-playbook
know that it
needs to decrypt one of the variable files.
ansible-playbook --ask-vault-pass local.yml
You can also place your Ansible Vault password in a file and tell Ansible to use that password file.
echo 'my$ecretpa$$' > ~/.vault_pass
ansible-playbook --vault-password-file ~/.vault_pass local.yml
You can avoid having to provide this flag altogether by defining the path to
your vault password file in an environment variable (defined in
~/.bash_profile
, or ~/.bashrc
).
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault_pass
Or if you want to make this a setting associated with your repository, you can
simply add a ‘[defaults]’ section to your ansible.cfg
that defines the path
to the vault password file.
[defaults]
. . .
vault_password_file = ~/.vault_pass
Keep in mind that recommend doing this only if you have a private repository that is not available to the public, but only to your development team. This makes it possible for all the secrets to be available to anyone on the team you give the password to.
Ansible Galaxy
Wouldn’t it be great if you could re-use roles that others have published?
This is possible! You can install and use roles from a public repository known as Ansible Galaxy. It’s also possible to obtain roles packaged on the web, or published in Git repositories (Github, Gitlab, or Bitbucket).
To do this, create a requirements.yml
file in the root of your Ansible
repository to store the roles that your configuration requires.
For our example we’ll use a role that sets the timezone
# Install timezone role
- src: yatesr.timezone
To install the required roles, run ansible-galaxy install -r requirements.yml
.
$ ansible-galaxy install -r requirements.yml
- downloading role 'timezone', owned by yatesr
- downloading role from https://github.com/yatesr/ansible-timezone/archive/1.1.0.tar.gz
- extracting yatesr.timezone to /home/ec2-user/.ansible/roles/yatesr.timezone
- yatesr.timezone (1.1.0) was installed successfully
Now that you’ve installed the timezone role, we’ll need to configure the timezone we want applied to our environment.
Simply add the timezone setting to your host_vars/local.yml
like below. We’re
using America/Los_Angeles
to configure the system to use the Pacific Daylight
Time (PDT) time zone.
For other time zone values, see List of TZ Database Time Zones - List.
Also feel free to uncomment your Git name and email address so that it applies like it did before.
git_client_name: PeeWee Herman
git_client_email: peewee@example.com
timezone: America/Los_Angeles
And of course we have to add the role to our playbook.
---
# This playbook deploys the entire setup to the Cloud9 development environment.
- hosts: local
roles:
- development
- yatesr.timezone
vars_files:
- ~/.ansible_secrets.yml
Let’s run a command to output our date/time, so we can compare it to what we see after reconfiguring our time zone.
$ date
Fri Mar 29 03:09:26 UTC 2019
Now let’s run our playbook again.
$ ansible-playbook local.yml
PLAY [local] *******************************************************************
TASK [Gathering Facts] *********************************************************
ok: [local]
TASK [development : upgrade all packages] **************************************
ok: [local]
TASK [development : debug] *****************************************************
ok: [local] => {
"ansible_user_id": "ec2-user"
}
TASK [development : configure git client] **************************************
ok: [local]
TASK [yatesr.timezone : include_vars] ******************************************
ok: [local] => (item=/home/ec2-user/.ansible/roles/yatesr.timezone/vars/../vars/RedHat.yml)
TASK [yatesr.timezone : Install tzdata for Debian based distros] ***************
skipping: [local]
TASK [yatesr.timezone : Install tzdata for RedHat based distros] ***************
ok: [local]
TASK [yatesr.timezone : Install tzdata for Archlinux based distros] ************
skipping: [local]
TASK [yatesr.timezone : Set timezone config] ***********************************
ok: [local]
TASK [yatesr.timezone : Set link to localtime] *********************************
ok: [local]
PLAY RECAP *********************************************************************
local : ok=8 changed=0 unreachable=0 failed=0
Now when we ask the system to output the date, we get the Pacific date/time.
$ date
Mon Mar 25 21:17:18 PDT 2019
See Ansible Galaxy Docs for more information.
Conclusion
I hope this guide has been useful in jumpstarting your interest in using Ansible. I surely tried to mention many of the things I needed to get started and start using Ansible.
I hope you’re more comfortable with Ansible and find yourself navigating the officialdocumentation with more ease now that you understand what it does and how to use it.
Feel free to refer to my cloud9-dev
repository to get other ideas on how
you can setup your local development environment in Cloud9 -
redconfetti/cloud9-dev.