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.
— 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:
- Create inventories for integration, staging and production
- Create a playbook to control the deployment
- Create all required tasks for our deployment
- 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.