Software deployment with Ansible

Some time ago Erika Heidi gave a talk at the PHP User Group Munich about Ansible and Vagrant. In a conversation we had afterwards she mentioned that Ansible is great for deployment too and that kind of stuck with me.

It took me a while but some days ago I made some changes to our deployment process. In this post I will share some basic concepts of these changes.

What is Ansible?

For those of you who don’t know Ansible.

Ansible: App deployment, configuration management and orchestration - all from one system. Powerful automation that you can learn quickly.

-- The Ansible Documentation

Ansible is an automation tool to remotely install and configure software like Puppet or Chef. Unlike those two, Ansible doesn’t need an installed daemon on the remote system to work, it just needs ssh access.

I won’t go into detail about Ansible and how to install and use it. Just this much, all you have to do, is to install Ansible and write some simple yaml configuration and Jinja2 template files. You can check out the Ansible documentation for more detailed information. Ansible is really easy to learn and it is awesome, not only for deployment.

Deployment

Before we really get started let me clarify the term deployment and what I’m talking about. For this article deployment is not about the tasks you execute to prepare your application. For example minimizing your Javascript or your CSS, executing Gulp or Sass, doing a composer install or other similar tasks. In this article deployment means things like copy code to your servers, create cache directories that are writable for the webserver and maybe restarting the web server and all the stuff that is required to make your application work on your servers.

The Plan

That said we can get started with outlining our deployment. We want a similar deployment process for all our different environments like integration, staging, and production. We want to create a directory on our web server for every version of our application so we can rollback or test different versions easily. We have to create some writable directories for caching purposes. Finally, we have to change the web server’s document root to the new directory and restart the web server.

So here is our Ansible ToDo list:

  1. Create inventories for integration, staging and production
  2. Create a playbook to control the deployment
  3. Create all required tasks for our deployment
  4. Allow quick rollbacks

Basic Setup

First of all, we have to create a basic Ansible file structure similar to this one. You can use this git repository I created on GitHub to get started.

ansible/
|- inventories/
|   |- integration/
|   |- production/
|   +- staging/
+- roles/
|   +- my-project/
+- play.my-project.yml

Usage of changing values

To deploy different versions with the same Ansible commands we have to pass some changing values over to Ansible. We will use Ansible’s extra-vars option to do this. So every time you see {{ version }} or {{ app }} in a YAML or Jinja2 example they refer to the version of our application we want to deploy and the path where the prepared application is located.

$ ansible-playbook --extra-vars "version=1.0.0 app=/some/local/path/to/your/app/"

Create the inventories

In our case, we use inventories to define settings that are different for each environment. As mentioned before we want to deploy our application to three different environments (integration, staging, and production). So we have to create an inventory for each one. We start with the list of web servers belonging to every environment.

[webservers]
192.168.1.111

To handle values that differ from environment to environment we have to define variables. Variables are loaded depending on groups and hosts. The easiest way to get started is to define our variables in an all group, so they are always loaded.

system:
  environment: integration
my_project:
  domain: www.my-project.int

You can now access your variables in any YAML or Jinja2 file like this.

{{ system.environment }}

Create a playbook

A playbook is the configuration file controlling the tasks Ansible executes. In our playbook, we define the group of hosts we want to execute the playbook and the roles we want to execute. In our case all web servers and the my-project role.

- hosts: webservers
  roles:
    - my-project

Create the deployment tasks

An Ansible role defines a list of tasks and tasks are the actual actions Ansible will perform on the configured hosts. So this is where the magic happens. Here we implement the previously outlined plan for our deployment.

# create the directory for the current app version
- name: Create version directory
  file: path=/var/www/my-project/{{ version }}
        state=directory

# copy the prepared app to your webservers
- name: Copy files to server
  synchronize: src={{ app }}
               dest=/var/www/my-project/{{ version }}
               perms=yes
               recursive=yes
               delete=yes
               owner=no
               group=no

# create a cache directory that is writable for the webserver
- name: Create cache directory
  file: path=/var/www/_cache_/my-project/{{ version }}
        state=directory
        group=www-data
        owner=www-data

# deploy the vhost configuration for our project
- name: Deploy vhost configuration
  action: template
          src=etc/apache2/sites-available/my-project.conf
          dest=/etc/apache2/sites-available/my-project.conf
  notify: restart apache2
  tags: rollback

# create a symlink to activate the vhost
# could be done with "command: a2ensite my-project.conf"
# but this would allways trigger an apache restart even if nothing changes
- name: Activate vhost configuration
  file: dest=/etc/apache2/sites-enabled/010-my-project.conf
        src=/etc/apache2/sites-available/my-project.conf
        state=link
  notify: restart apache2

Create the handlers

To actually restart the web server we have to define an Ansible handler. A handler gets triggered by the notify value and will be executed once all tasks are done, even if multiple tasks notify the same handler.

- name: restart apache2
service: name=apache2 enabled=yes state=restarted

Create rollback tasks

For our rollback, we don’t have to create new tasks. Everything we need is already there. Since we already copied the files and created the cache directories for the version we want to roll back to, we just have to update the vhost configuration and restart the web server.

To achieve this we use tags. Tags are great to execute only tasks marked with a certain tag within a playbook. So tasks without the requested tag won’t be executed. For our purposes, we will use the tag rollback.

If we pass the tag rollback to Ansible only one task of our playbook will be executed. The one changing the vhost configuration and triggering the web server restart.

Execute the playbook

Now we are ready to execute the playbook to deploy our code.

$ ansible-playbook -i inv/integration/hosts --extra-vars "version=1.0.1 app=/some/local/dir/app/" play.my-project.yml

Or to do a quick rollback

$ ansible-playbook -i inv/integration/hosts --extra-vars "version=1.0.0" --tags rollback play.my-project.yml

There is much more you can do. For example deploying database or service credentials based on encrypted files using Ansibles’s Vault feature. Or improve the rollback task with previous checks that make sure the directories exist. You could add another vhost template listening to {{ version }}.www.my-project.de to preview the new version and so on and so forth. Let your imagination run free.

I hope I could encourage you to give Ansible a try to automate your deployment process. You have any further remarks? Let me know in the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *