Getting Started With Ansible
An introduction to Ansible
- Why Ansible?
- My Requirements
- Part 1: Preparing my Inventory
- Part 2: Configuring Users and Groups with Modules
- Part 3: Automating Tasks Using Playbooks
- Part 4: Using Templates To Setup Privileges and SSH Security
- Part 5: Source Control
- Summary
Why Ansible?
In short: simplicity. Ansible is the kind of tool that you just pick up and use; the learning curve is minimal. All the communication is handled via vanilla SSH so there's no clients to install and it uses a push system for deploying changes, so no central server is required. This makes the configs portable and the setup costs low. The Playbooks (like Cookbooks in Chef) are written in YAML and the template engine uses Jinja2 - tools I'm very familiar with. Lastly, and perhaps for me the biggest draw card, the source code is written in Python.
My Requirements
Before beginning my Ansible journey, I set aside some goals to guide the process:
- I should never, ever have to log into a production server to configure it. That means everything down to the first user account should be handled by Ansible.
- Everything configured should be completely self documenting. If I come back to them after a year, it should be very clear what everything does.
- The entire system should be extremely portable. Any time I need to
rebuild my development server, the configs should just be a
git pullaway.
Which that in mind, I documented the steps I followed to get the basics up and running: user accounts, groups, sshd_config and sudoers. I have been able to get much further than this in a very short period of time (thanks largely to Ansible's excellent documentation and ease of use), in the interest of brevity, let's keep it...brief.
Part 1: Preparing my Inventory
I started by creating a new Ubuntu VM at Linode and setting a default password.
Then, on my development machine, I installed Ansible's prerequisties: PyYAML, Jinja2 and Paramiko.
lex@desktop:~> sudo pip install pyyaml jinja2 paramiko
Note: it is trivial to run Ansible in a virtualenv container and generally a good idea. For simplicity sake, I've skipped those steps here.
Next, following Ansible's install instructions, I cloned the repo, checking out the latest stable branch and installed it using distutils.
lex@desktop:~> cd src
lex@desktop:~/src> git clone -b release0.9 git://github.com/ansible/ansible.git
lex@desktop:~/src> cd ansible
lex@desktop:~/src/ansible> sudo python setup.py install
After, I created the /etc/ansible/ directory which housed my first
hosts file, initially just listing my created VM (but would soon
list all my servers in production).
/etc/ansible/hosts
[webservers]
prodweb03
The host file can also include additional per-host and per-group variables which are useful for configuring things like network settings and so on. Consult the Inventory and Patterns section of the doco for more info.
I then tested connectivity to the web servers using the ping module
(more on Ansible's modules and syntax later).
lex@desktop:/etc/ansible> ansible all -m ping -u root --ask-pass
SSH password:
prodweb03 | success >> {
"changed": false,
"ping": "pong"
}
Part 2: Configuring Users and Groups with Modules
Ansible has a reasonable library of modules built-in which can be executed via Playbooks or directly via the command line. The latter's syntax looks a bit like this:
lex@desktop:~> ansible <host_pattern> -m <module_name> -a <module_args> <additional_args>
For example, I can utilise the command module to run uptime across
my hosts.
lex@desktop:/etc/ansible> ansible webservers -m command -a "uptime" -u root --ask-pass
SSH password:
prodweb03 | success | rc=0 >>
06:55:23 up 12 min, 1 user, load average: 0.00, 0.01, 0.01
Note that by default, Ansible attempts to perform operations as the
local user, using public key authentication. Since I don't have any
users setup remotely just yet, nor any keys, I'll have to explicitly run
the command as root (-u root) forcing a password prompt
(--ask-pass).
To start the configuration process, I configured my user accounts using
the group and user modules, with the intention of later moving the
commands to Playbooks.
Here I created a group called admin, with a GID of 1000.
lex@desktop:/etc/ansible> ansible webservers -m group -a "gid=1000 name=admin state=present" -u root --ask-pass
Hopefully, the arguments I'm passing to the module are self-explanatory. If not, consult the amazingly simple documentation.
Next, I created my user account with the user module, and uploaded my
authorized_key file using the module with the same name.
lex@desktop:/etc/ansible> ansible webservers -m user -a \
"name=lex group=admin shell=/bin/bash uid=1000" -u root --ask-pass
lex@desktop:/etc/ansible> ansible webservers -m authorized_key -a \
"user=lex key='$(cat ~/.ssh/id_rsa.pub)'" -u root --ask-pass
Now, since I have ssh-agent running, I can perform commands across the
server(s) as myself.
lex@desktop:/etc/ansible> ansible webservers -m command -a "uptime"
prodweb03 | success | rc=0 >>
07:31:47 up 48 min, 1 user, load average: 0.00, 0.01, 0.02
Part 3: Automating Tasks Using Playbooks
Playbooks are YAML configuration files that define a set of tasks or
Plays to be executed on the remote server. Following Ansible's Best
Practises document, I created a company_name directory to house my
Playbooks and additional files. Then, I created the first Playbook,
initial.yml, where I would store the user and group creation tasks I
ran manually earlier.
lex@desktop:/etc/ansible> mkdir lexandstuff
lex@desktop:/etc/ansible> cd lexandstuff/
lex@desktop:/etc/ansible/lexandstuff> vi initial.yml
/etc/ansible/lexandstuff/initial.yml
- hosts: webservers
sudo: yes
tasks:
# ===============================================================
# Configure user's and groups
# ===============================================================
- name: create admin group
action: group name=admin gid=1000 system=no
- name: create user(s)
action: user name=lex group=admin shell=/bin/bash uid=1000
- name: setup authorized key(s)
action: authorized_key user=lex key='$FILE(/home/lex/.ssh/id_rsa.pub)'
The syntax is quite self-explanatory: hosts represents the group of
servers this Playbook will refer to. sudo defines whether or not we'll
run the commands using sudo. Then, in the tasks section, name is the
human-readable task description that appears on the command line while
the Playbook is running. Clearly then, action represents the actual
task.
With that complete, I can now run the Playbook at the prompt and watch the magic happen.
lex@desktop:/etc/ansible/lexandstuff> ansible-playbook initial.yml -u root --ask-pass
SSH password:
PLAY [webservers] *********************
GATHERING FACTS *********************
ok: [prodweb03]
TASK: [create group(s)] *********************
ok: [prodweb03]
TASK: [create user(s)] *********************
ok: [prodweb03]
TASK: [setup authorized key(s)] *********************
ok: [prodweb03]
PLAY RECAP *********************
prodweb03 : ok=4 changed=0 unreachable=0 failed=0
Perhaps at some stage I expect to require more than just a single account and user group on my production web servers. One way to handle it, is to just list additionally actions in order.
/etc/ansible/lexandstuff/initial.yml
#...
- name: create admin group
action: group name=admin gid=1000 system=no
- name: create sshusers group
action: group name=sshusers gid=1001 system=no
#...
Or, I could separate a repeatable task into a separate file and call it from within my main Playbook.
/etc/ansible/lexandstuff/initial.yml
#...
- include: tasks/create_user.yml user=lex group=admin full_name='Lex Toumbourou' uid=1000
- include: tasks/create_user.yml user=travis group=sshusers full_name='Travis Bickle' uid=1001
- include: tasks/create_user.yml user=jacob group=sshusers full_name='Jacob Singer' uid=1002
#...
/etc/ansible/lexandstuff/tasks/create_user.yml
- name: create user(s)
action: user name=$user group=$group shell=/bin/bash uid=$uid
- name: setup authorized key(s)
action: authorized_key user=$user key='$FILE(/home/$user/.ssh/id_rsa.pub)'
Part 4: Using Templates To Setup Privileges and SSH Security
Ansible relies on the very powerful Jinja2 engine to handle templating. In this last part of the tutorial, I'm going to utilise templates to create a custom sudoers file, allowing me to run commands on the remote hosts as myself, and a customised sshd config.
Firstly, I created the templates directory and wrote the custom sudoers
template. You may note that initially, I'm not taking advantage of the
templating engine at all. I could have simply used the copy module to
copy the file up to my server, but this gives me a chance to extend it
later.
/etc/ansible/lexandstuff/templates/custom_sudo.j2
# Members of the admin group may gain root privileges
%admin ALL=(ALL) NOPASSWD:ALL
Now, I can import add the creation of /etc/sudoers.d/custom into the
Playbook.
/etc/ansible/lexandstuff/initial.yml
#...
tasks:
#...
# ===============================================================
# Access, security and permissions
# ===============================================================
- name: write the sudoers file
action: template src=templates/sudoers.j2 dest=/etc/sudoers.d/custom
owner=root group=root mode=0400
#...
Now, for my sshd_config, I created another template with placeholders
for the variables configured in the Playbook, using Jinja2's
{{ variable_name }} syntax.
/etc/ansible/lexandstuff/templates/sshd_config.j2
Port {{ ssh_port }}
Protocol {{ ssh_protocol }}
#...
RSAAuthentication {{ ssh_rsa_authentication }}
PubkeyAuthentication {{ ssh_public_key_authentication }}
#...
And, so on.
In a separate vars file, I specified the variables...
/etc/ansible/lexandstuff/vars/defaults.yml
########################
# sshd_config settings
########################
ssh_port: 22
ssh_protocol: 2
ssh_syslog_facility: AUTH
#...
# Allow authentication methods
ssh_rsa_authentication: yes
ssh_public_key_authentication: yes
#...
...which were included in the Playbook using the vars_files parameter.
/etc/ansible/lexandstuff/initial.yml
# ...
- hosts: all
sudo: yes
gather_facts: no
vars_file:
- vars/defaults.yml
tasks:
# ...
Then, I just call the template module to create the sshd_config
file.
/etc/ansible/lexandstuff/initial.yml
# ...
- name: write the sshd_config file
action: template src=templates/sshd_config.j2 dest=/etc/ssh/sshd_config
owner=root group=root mode=0644
In order for the changes to take effect, we're going to need to reload
the ssh daemon after changing the config. That can be done with a
handler. A handler is a list of tasks that another task can call or
"notify" to perform after the Playbook has finished executing. So, in
the config, I've added a handler section toward the bottom which uses
the service module to reload the config.
/etc/ansible/lexandstuff/initial.yml
#...
handlers:
- name: reload sshd
action: service name=ssh state=reloaded
Then, using the notify parameter, I can call the handler after the
sshd config file is generated.
/etc/ansible/lexandstuff/initial.yml
#...
- name: write the sshd_config file
action: template src=templates/sshd_config.j2 dest=/etc/ssh/sshd_config
owner=root group=root mode=0644
notify:
- reload sshd
And we're done.
Part 5: Source Control
Lastly, I put my entire repo in source control.
lex@desktop:/etc/ansible/lexandstuff> git init
Initialized empty Git repository in /etc/ansible/lexandstuff/.git/
lex@desktop:/etc/ansible/lexandstuff> git add .
lex@desktop:/etc/ansible/lexandstuff> git commit -m "First working version configures basic environment"
I then pushed it to a private repo on GitHub, which means that I have access to them from anywhere.
Summary
In conclusion, Ansible is awesome. Straight-forward, easy, self-documenting and, dare I say it, fun. If you're finding yourself putting off automating configuration management because you're fearing a steep learning curve, then Ansible is probably for you.