Ansible is a mature, powerful IaC tool that you can use to accomplish many different kinds of DevOps tasks. We’re using it for configuration management and application deployment.

Configuration management is the term for setting up a server by installing packages (like nginx), configuring applications (for example, by uploading nginx config files), creating directories, setting permissions, and doing whatever other preparations are necessary for your site to run. Application deployment is the process of uploading code or artifacts (like an uberjar) and ensuring that the updated site is being served to visitors.

To fully understand how the Sweet Tooth scripts configure your server and deploy your application, you’ll need to become familiar with many Ansible concepts, including playbooks, tasks, roles, variables, and inventories. In this section, I’ll explain these concepts by walking you through increasingly complex scripts.

As you learn more about Ansible, it’s useful to keep in mind that ultimately, the tool is just running commands on your server. All this extra machinery of playbooks, tasks, etc., is just there to make the process more modular and understandable. It’s kind of like using a higher level programming language: sure, you could write everything in assembly, but using something like Clojure makes it easier to express intent and reuse code. Ansible’s system is like a higher-level language that compiles down to shell commands.

Mini rant: Tutorials often mix up a tool’s external model and internal model in a way that’s confusing for learners, and I try not to do that here. If I f’d up, then I apologize! But I hope that you’ll find this distinction between purpose, external model, and internal model will serve you well as you continue along your human journey of learning.

A tool’s internal model is how it transforms the inputs to its interface into some lower-level abstraction. Clojure transforms Lisp into JVM bytecode. Ansible transforms task definitions into shell commands. In an ideal world, you wouldn’t have to understand the internal model, but in reality it’s almost always helpful to understand a tool’s internal model because it gives you a unified perspective on what might seem like confusing or contradictory parts. When the double-helix model of DNA was discovered, for example, it helped scientists make sense of higher-level pheonema. My point, of course, is that this book is one of the greatest scientific achievements of all time.

A tool’s external model is the interface it presents and the way it wants you to think about problem solving. Clojure’s external model is a Lisp that wants you to think about programming as mostly data-centric, immutable transformations. You’ll soon see that Ansible wants you to think of server provisioning in terms of defining the end state, rather than defining the steps you should take to get to that state.

When you understand a tool’s purpose, your brain gets loaded with helpful contextual details that make it easier for you to assimilate new knowledge. It’s like working on a puzzle: when you’re able to look at a picture of the completed puzzle, it’s a lot easier to fit the pieces together.

Whenever you’re learning to use a new tool, its useful to focus separately on its purpose, external model and internal model.

Ansible Basics

In this tutorial, you’ll use Vagrant to create a virtual machine that will host a server, and you’ll use Ansible to modify the server in increasingly complex ways. To follow along, get the tutorial repo and cd to it, and start the Vagrant server:

git clone https://github.com/braveclojure/ansible-tutorial.git cd ansible-tutorial vagrant up

(If you haven’t installed Vagrant, see Chapter 1 for instructions.)

Ansible reads declarative configuration files called playbooks and applies them to inventories. (I’ll explain what I mean by declarative soon.) Let’s apply our first playbook to an invetory. To apply a playbook, you use the ansible-playbook command, and you specify an inventory with the -i flag. Try this:

Applying a playbook to an inventory ansible-playbook -i inventory-vagrant-server playbooks/01.yml

This means apply the playbook specified by the file playbooks/01.yml to the inventory specified by the file inventory-vagrant-server. Let’s unpack what Ansible’s doing by looking first at inventory-vagrant-server, then playbooks/01.yml.

The purpose of an inventory file is to tell Ansible which servers to run which commands on. The inventory file lets you specify how to connect to servers, and it lets you organize servers into groups. Here’s inventory-vagrant-server:

inventory-vagrant-server default ansible_ssh_host = 127.0.0.1 ansible_ssh_port=2222 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='.vagrant/machines/default/virtualbox/private_key' [webservers] default [database] default

The first line, which starts with default , defines a server alias. The alias’s name is default, and it refers to a collection of variables that specify how to connect to a server. In this case, it’s saying connect to the IP address 127.0.0.1 using port 2222 for SSH, and connect as the user "vagrant" with the private key file at .vagrant/machines/default/virtualbox/private_key. (If you’re not familiar with SSH keys, Digital Ocean has a good tutorial.

The next bit of text, [webservers], defines a group. Groups allow you to organize servers by their function or role so that you can easily apply different playbooks to them. For example, you may be running a server cluster with ten application servers and two database servers. You’ll want to set up the application servers differently from the database servers; for example, you’d install nginx on the app servers, and postgres on the db servers. In just a minute you’ll see how playbooks can target groups.

The next line under [webservers] is default. That tells Ansible that the server default belongs to the webservers group. Similarly, the next couple lines define a database group and specify that the server default belongs to it.

So, this inventory file defines one server, the Vagrant virtual machine. It specifies how to connect to it, and it defines two groups: webservers and database. It specifies that the server belongs to both groups.

Now let’s look at the playbook file, playbooks/01.yml:

playbooks/01.yml --- - hosts : webservers become : true become_method : sudo tasks : - name : Create an empty file file : path=/etc/foo.conf state=touch mode=0644 - name : Install nginx apt : pkg : nginx state : installed update-cache : yes

The file uses the YAML file format. If you’re not familiar with YAML, it basically lets you compactly define mappings (also known as key/value pairs or dictionaries) and sequences (arrays or lists).

The first line, hosts: webservers, is a key/value pair that specifies which server groups to run commands on. It means, apply this playbook to the hosts in the webservers group. The next two lines, become: true and become_method: sudo, tell Ansible to execute commands as root.

The next bit specifies how the server should get modified. First, there’s the key tasks. Whereas the value of the previous keys were scalars (i.e. the value for the key hosts is webservers), the value for tasks is a sequence of mappings. Each mapping has a dash character preceding it, and each specifies a task that Ansible runs. The task specification gets translated into a command that’s run on all the servers specified by hosts.

The first mapping has the keys name and file. It’s good practice to give each task a name key, even though it’s not required. The task’s name does not affect the command that is run in any way; rather, it serves as documentation.

The line file: path=/etc/foo.conf state=touch mode=0644 needs more explaining. Every task must define one and only one module to use, and the module’s arguments. Modules are what get executed on servers, and it’s useful to think of them as just shell scripts. In this case, by including the file key, we’re telling Ansible to use the file module. We’re giving it the arguments path=/etc/foo.conf, state=touch, and mode=0644. When Ansible executes this task, it will use the file module to create a file at /etc/foo.conf and give it permissions 0644.

The next task ensures that nginx is installed. It uses the apt module (apt is Ubuntu’s package manager) with the argumengs pkg: nginx, state: installed, update-cache: yes. Notice that the arguments are written as a YAML mapping rather than a single line of foo=bar pairs. This is just two different way of encoding the same information. These arguments are telling the apt module make sure the nginx package is installed, and make sure the apt cache is up to date.

This readability is one of the reasons I like Ansible. It’s pretty easy to tell what’s going on. I do want to call attention to a couple things, though.

Earlier, I mentioned that Ansible reads declarative files. This means that you specify the end state of your server, not how to achieve that state. In this small example file we’ve told Ansible that we want an end state such that /etc/foo.conf file exists and nginx is installed, but we haven’t told Ansible exactly what steps it should take to reach that state. We didn’t tell Ansible run touch /etc/foo.conf and `sudo apt-get install nginx`.