Ansi-strano - A Capistrano like deployment process using Ansible

Posted on 06/25/2016 by Brian Carey


Welcome back to the KISS IT blog!  Sorry for the extended hiatus but we've been very busy.  Today we're bringing you a post covering a recently created tool we've added to our arsenal, Ansi-strano.  Ansi-strano is a Capistrano like deployment process using Ansible that ties the power of Ansible's inventory management and automation with a release process similar to that of Capistrano.

Here at KISS, we prefer simple, automated deployment processes that result in the same tasks being performed exactly the same for each deployment run.  More recently it seems as though there is a trend towards using "Containers" and "Immutable Infrastructure" as a crutch to solve problems with inconsistent deployments.  I say crutch because I'm not sure why this is considered easier and less error prone.  With a solid automated deployment process all deployments can be done safely and reliably all day long without impact to users.

Overview

For several years we had been using a tool named Caphub which is based on Capistrano for our deployment needs.  This allowed us to have a single deployment project for a given client that housed all of their deployment processes and configuration needed for one or more applications.  Capistrano has some great concepts which include but are not limited to:

  • Symlinked configuration files and shared directories that allow for assets to be stored outside of the application directory on a server but included at deploy time.
  • Symlinked releases that allow for all release tasks to be performed prior to activating a release only if all tasks are successful.
  • Keeping a history of releases on each server and the ability to easily rollback to a previous release (partly due to the concept of symlinked releases).

However, one thing that Capistrano seemed to be lacking was a simple way to manage a large infrastructure as a deployment target, especially in cases when this infrastructure was dynamic.  With our recent standardization on Ansible for all of our system automation needs, it didn't take long to start to want to use Ansible for application deployments due to its powerful inventory management options.  However, we didn't want to lose the existing deployment features of Capistrano.   Thus, we created the tool we're covering today, Ansi-strano.

Requirements

These are covered in the Readme, but I'll also list them here for completeness:

  • First and foremost, you need Ansible installed and configured for your environment. If you already have inventory setup or being pulled from a dynamic cloud script you can easily drop this in and configure it to use what you have.
  • The user that you're using to run ansible across the hosts must have sudo access.  This is probably already required for other things you do with Ansible.
  • A github repo containing the application to be deployed.

Deployed Application Directory Structure

Before going any further, its important to understand the structure that is created on a server as part of this deployment process.  In this example, we're deploying to the base directory /var/www/kiss-ci-base.  The following shows the directory structure within this location:

current -> /var/www/kiss-ci-base/releases/20160625212658
releases/
    20160625212658/
    20160625221020/
    20160625224058/
    20160625224414/
repo/
    .git
    .gitignore
    Codeigniter-license.txt
    LICENSE
    README.md
    application/
    public/
    sql/
    system/
shared/
    cache/
    config.php
    database.php
    logs/

The above structure is broken down as follows:

  • The current symlink is the main pointer to the current release.  For instance, in our case we have Apache configured to serve the path /var/www/kiss-ci-base/current.  
  • The releases directory contains each release deployed via the tool, each in a directory named based on the date/time of the release.  You'll notice that the current symlink points to the latest release.
  • The repo directory is where we check out the Github repo.  We keep this here across releases so that each release only requires pulling in the changes since the last release as opposed to cloning the full repo every release.
  • The shared directory is where we create the various static directories that are symlinked into each release.  This is also where the configuration templates are written to and likewise symlinked into each release

Installing and Configuring Ansi-strano

To install the tool, first clone or download the code from the repo: https://github.com/kissit/ansi-strano.  Once you've done so, you'll need to adjust at least some of the configuration for your environment.  The options are all located in the file group_vars/all by default.  For very basic uses this may be fine, however in larger real-world environments the options may need to be set across multiple group_vars files based on your various classes of servers and the options that may need to vary from one environment to another.

  • hosts - This is the standard Ansible option for which hosts should be targeted for deployment.  There are various ways this can be configured (static inventory, dynamic inventory & groups, etc).
  • repo_url - This needs to be set to the Github repo that contains the application being deployed.
  • keep_releases - This is the number of past releases to keep available for rollback in the deployment structure on each system being deployed.  By default this is set to 6.
  • deploy_base - This is the base directory where the application will be deployed to.  Within this directory will be the above directory structure.  You'll definitely want to change this to be where you want things to be deployed to.
  • shared_path - This is the directory where things like log directories and config files will be placed and symlinked to.  You probably don't need to change this as its based on the deploy_base.
  • current_path - This is the name of the "current" active release symlink.  You probably don't need to change this as its based on the deploy_base.  However, this may be the most important piece to remember as it would be the location where your application is served from.  For example if using Apache this may be configured as a Vhost directory to be served.
  • repo_path - This is the work directory where the actual repo is cloned/updated.  You probably don't need to change this as its based on the deploy_base.
  • file_owner - This is the user that will be set to own the deployed files.  By default its set to root but you can change to suit your needs.
  • file_group - This is the group that will be set to own the deployed files.  By default its set to wheel but you can change to suit your needs.
  • restart_services - This is a list of services to be restarted at the end of the deployment.  Leave this blank for none.
  • config_files - This is a hash of configuration files to be "templated" as part of the deployment process.  In this example, we're deploying a CodeIgniter application and using database.php and config.php.  The variables that are used to populate the various options in the files are set in the standard Ansible fashion in group_vars/all, though in a real world scenario these would most likely be set differently in different group_vars files based on environment, etc.
  • symlinks - This is a hash of files/directories to be symlinked as part of the release process.  In most cases the sources will exist in the "shared" directory.  This is so that things like log directories can stay consistent across releases, and also so that the configuration files can live outside of the release directory.  In this example we're symlinking our config files from the previous step as well as the logs and cache directories.

The default configuration is as follows:

## These vars control the behavior of the deploy/rollback processes.  See the Readme for details.
repo_url: git@github.com:kissit/kiss-ci-base.git
keep_releases: 6
deploy_base: /var/www/kiss-ci-base
shared_path: "{{ deploy_base }}/shared"
current_path: "{{ deploy_base }}/current"
repo_path: "{{ deploy_base }}/repo"
file_owner: root
file_group: wheel
restart_services:
  - apache24
config_files:
  - { src: "templates/database.php", dest: "{{ shared_path }}/database.php" }
  - { src: "templates/config.php", dest: "{{ shared_path }}/config.php" }
symlinks:
  - { src: "{{ shared_path }}/database.php", dest: "{{ release_path }}/application/config/database.php", create_src: False}
  - { src: "{{ shared_path }}/config.php", dest: "{{ release_path }}/application/config/config.php", create_src: False}
  - { src: "{{ shared_path }}/logs", dest: "{{ release_path }}/application/logs", create_src: True}
  - { src: "{{ shared_path }}/cache", dest: "{{ release_path }}/application/cache", create_src: True}

## These vars are used in the templates for the config files.  In real world scenarios these may be split out into multiple group_vars files
## in order to set different values based on environment being targetted.
ci_base_url: http://kiss-ci-base
ci_db_host: localhost
ci_db_name: xxxxxxxxx
ci_db_user: xxxxxxxxx
ci_db_pass: xxxxxxxxx

Running a Deployment

Running a deployment is as simple as running any other Ansible playbook by using the ansible-playbook command.  In our example, we're only deploying to our localhost for demonstration purposes, but in a real world scenario we'd have the hosts var set to one or more ansible groups that should receive the deployment.  Anyway, here is how we run the default deployment and an example of the output.

[brian@freebsd-local ~/ansi-strano]$ ansible-playbook ansi-strano-deploy.yml

PLAY [Ansi-strano-deploy - Capistrano like deployment process using Ansible] ***

TASK: [Generate release timestamp] ********************************************
changed: [localhost -> 127.0.0.1]

TASK: [set_fact release_path='{{ deploy_base }}/releases/{{ timestamp.stdout }}'] ***
ok: [localhost]

TASK: [set_fact branch=master] ************************************************
ok: [localhost]

TASK: [Ensure our release related directories exist] **************************
ok: [localhost] => (item=/var/www/kiss-ci-base)
changed: [localhost] => (item=/var/www/kiss-ci-base/releases/20160626180628)
ok: [localhost] => (item=/var/www/kiss-ci-base/shared)
ok: [localhost] => (item=/var/www/kiss-ci-base/repo)

TASK: [Update source git repo] ************************************************
ok: [localhost]

TASK: [Copy the cached git copy] **********************************************
changed: [localhost]

TASK: [git checkout our desired branch] ***************************************
changed: [localhost]

TASK: [Update our config files if desired] ************************************
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/shared/database.php', 'src': 'templates/database.php'})
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/shared/config.php', 'src': 'templates/config.php'})

TASK: [Ensure our various symlink sources exist] ******************************
skipping: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/config/database.php', 'src': u'/var/www/kiss-ci-base/shared/database.php', 'create_src': False})
skipping: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/config/config.php', 'src': u'/var/www/kiss-ci-base/shared/config.php', 'create_src': False})
ok: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/logs', 'src': u'/var/www/kiss-ci-base/shared/logs', 'create_src': True})
ok: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/cache', 'src': u'/var/www/kiss-ci-base/shared/cache', 'create_src': True})

TASK: [Create our symlinks] ***************************************************
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/config/database.php', 'src': u'/var/www/kiss-ci-base/shared/database.php', 'create_src': False})
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/config/config.php', 'src': u'/var/www/kiss-ci-base/shared/config.php', 'create_src': False})
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/logs', 'src': u'/var/www/kiss-ci-base/shared/logs', 'create_src': True})
changed: [localhost] => (item={'dest': u'/var/www/kiss-ci-base/releases/20160626180628/application/cache', 'src': u'/var/www/kiss-ci-base/shared/cache', 'create_src': True})

TASK: [Activate the release] **************************************************
changed: [localhost]

TASK: [Restart services as configured] ****************************************
changed: [localhost] => (item=apache24)

TASK: [Cleanup old releases] **************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=13   changed=9    unreachable=0    failed=0


We also may choose to further limit the target hosts using the --limit flag.  For instance, in this case we only want to target the hosts that are also in the production group.

[brian@freebsd-local ~/ansi-strano]$ ansible-playbook ansi-strano-deploy.yml --limit=production

Rolling Back a Deployment

In a perfect world, the changes being deployed have been thoroughly tested and once deployed everything is working as expected.  However, most of us don't work in a perfect world and sometimes releases just go bad.  The good news is there is a similar playbook to use to roll back to a previous release that is still available on the target systems.  In most cases, this will be the last working release before your recent deployment.  This is where its recommended to keep your keep_releases setting set to something reasonable that would span say at least a few days of releases based on how frequently you do.

In the following example, we will roll back to the release dated 20160625224414.  When you run the rollback playbook, it will prompt you for this release name.

[brian@freebsd-local ~/ansi-strano]$ ansible-playbook ansi-strano-rollback.yml
Please enter the revision name to revert to.  This is the dated folder in the releases directory. [x]: 20160625224414

PLAY [Ansi-strano-rollback - Capistrano like rollback process using Ansible] ***

TASK: [Validate the release name first] ***************************************
ok: [localhost]

TASK: [Revert the symlinks to the specified version] **************************
changed: [localhost]

TASK: [Restart services as configured] ****************************************
changed: [localhost] => (item=apache24)

PLAY RECAP ********************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0

After running the rollback task, if you check your current symlink, you'll see that it has been swapped back to the specified release.  Also, the release that you rolled back from is left in place for reference.

[brian@freebsd-local ~/ansi-strano]$ ll /var/www/kiss-ci-base/current
lrwxr-xr-x  1 root  wheel  45 Jun 26 18:18 /var/www/kiss-ci-base/current -> /var/www/kiss-ci-base/releases/20160625224414

Conclusion

In conclusion, we hope you find this process as simple and powerful as we do.  We've been using this general process for deploying all sorts of applications for years with great success.  Its not limited to simple web apps served by Apache, any application that will play well with symlinks should be able to be deployed with this process and customizing the process is as simple as updating the playbook as needed to accomplish what you require.  Thanks again for taking the time to review our work and as always, please don't hesitate to contact us with any questions or comments regarding this post.