Easily deploy web applications on multiple web servers with Ansible and Ansistrano!

At my previous work we’ve been using Capistrano for years and it’s not particularly great. It does work well, but it really feels like messy Ruby scripting with messy documentation.

We use Capistrano for a specific kind of projects which are usually PHP or Node deployed on multiple non-autoscaled webservers.

Recently for school I had two Laravel projects to make. One of the requirements was to deploy them on a real webserver with a valid domain name, HTTPS, etc.

I’ve deployed PHP software countless times, even for myself, so this wasn’t a hurdle. However after manually installing everything and doing git pull every time I wanted to “deploy” a new release, I figured I could do better, even if it was technically sufficient for my school project.

That was a good opportunity to try Ansistrano! Ansistrano is actually an Ansible role, but it’s messy in its own way because IMHO it’s not how Ansible is supposed to work.

Usually Ansible is supposed to be idempotent, which means if I run the same task twice, nothing will change the second time. Ansistrano is not idempotent in any way because it will make a new release every single time even if the source code didn’t change. I don’t mean to say this is a bad way of doing things though, because Ansistrano works really well. It’s just that it feels a bit hacky (but it’s still more pleasant to use than Capistrano).

The good thing about being an Ansible role though is that it integrates very well with existing Ansible configuration.

In my case, I had to deploy two Laravel projects on a single webserver. Luckily I already have a bunch of Ansible roles lying around so I was able to setup the whole middleware stack with Ansible!

That being done, I made another playbook for the application deployment with Ansistrano.

It’s actually mostly variables:

---
- name: "Deploy {{ app_name }} laravel app"
  hosts: ffw
  vars:
    ansistrano_deploy_to: "/srv/{{ app_name }}"
    ansistrano_keep_releases: 3
    ansistrano_deploy_via: git
    ansistrano_git_repo: "https://github.com/fight-food-waste/{{ app_name }}.git"
    ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/tasks/pre-deploy.yml"
    ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/tasks/post-deploy.yml"

  roles:
    - { role: ansistrano.deploy, tags: deploy }

My git repositories are public so it’s even more straightforward. By the way Ansistrano supports many other sources such as SVN, rsync, HTTP, S3…

Then I’ll deploy my app with:

$ ansible-playbook pb-laravel-app.yml -e app_name=collects

On the server, the folder architecture will look like that:

-- /srv/collects
|-- current -> /serv/collects/releases/20190512131539
|-- releases
| |-- 20190512131539
| |-- 20190509150741
| |-- 20190509145325
|-- shared

Every time the role is run, a new directory is created inreleases and the symlink to current is updated.

There are multiple phases during an Ansistrano deployement:

The Ansistrano workflow

The Ansistrano workflow

You can import tasks files using variables, for example ansistrano_before_setup_tasks_file which will be the first step of all the workflow. In this case I install composer before setup and I run composer install after symlink (along with a bunch of other tasks). What’s really cool is that you can use any Ansible task you want, which I find far better than using run to pass shell commands with Capistrano.

Overall I’m very satisfied with Ansistrano and I will definitely use it again if it fits the use case.

You can find my whole Ansible configuration example at fight-food-waste/deploy.