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.
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:
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.
These are covered in the Readme, but I'll also list them here for completeness:
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:
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.
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 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
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
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.