Deploying a Symfony application with Ansible

I’ve already covered how to deploy a Symfony application with Magallanes and how to deploy a Symfony application with Capistrano. In this story, I’m going to describe how to deploy that application with Ansible, a tool that will allow you to automate your apps and your IT structure.

The requirements

First of all, read. You must know a bit about Ansible before continuing this story and you must know what the hell I’m talking about. Playbooks, inventory and roles should be familiar words from now on. But, what do you need in order to deploy a Symfony application with Ansible? Easy stuff: just install Ansible. Read this website for the instructions. Have you already finished? Ok, let’s continue.

The roles

Carlos Buenosvinos, a well-known professional in the PHP environment, created an Ansible role for web application deployments: https://github.com/ansistrano/deploy. You can use that role and create some Symfony custom tasks in order to get your Symfony application up and running. But, there is another role that has Carlos Buenosvinos’ deploy role as a dependency: https://github.com/cbrunnkvist/ansistrano-symfony-deploy.

So, let’s install that role that will help us a lot to deploy our Symfony application. Use the following command:

$ ansible-galaxy install cbrunnkvist.ansistrano-symfony-deploy

Is that easy? Yes it is, for sure. Let’s continue with the “difficult” stuff.

Configuring Ansible

Because we are configuring something, I usually create an etc folder. Inside it, create an etc/ansible folder. Now, we must create a single file per host. Let’s create a prod file inside etc/ansible with the following content:

[prod-webservers]
prod.domain.com ansible_user=sshuser

[prod-dbservers]
prod.domain.com ansible_user=sshuser

[prod:children]
prod-webservers
prod-dbservers

Feel free to edit the content of the file, indeed! Now, I suggest you to create an ansible.cfg file in the root of your project with the following content:

[defaults]
transport = ssh

[ssh_connection]
ssh_args = -o ForwardAgent=yes

This will allow you to forward your ssh key when deploying with Ansible.

Configuring Ansistrano

Last part: let’s configure the deployment. Be sure to read both roles already mentioned, or you will be lost on some default files and variables.

Create an etc/deploy folder and, inside it, an etc/deploy/deploy.yml file with the following content:

---
-
name: Deploy Application
hosts: all
gather_facts: false
vars:
ansistrano_deploy_from:
"{{ playbook_dir }}/../../"
ansistrano_deploy_to: "{{ deploy_to }}"
ansistrano_keep_releases: 3
ansistrano_deploy_via: "git"
ansistrano_git_repo: git@gitlab.com:organization/project.git
ansistrano_shared_paths:
- var/logs
- var/sessions
ansistrano_shared_files:
- app/config/parameters.yml
symfony_console_path: 'bin/console'
symfony_run_assetic_dump: false
symfony_run_doctrine_migrations: true
roles:
- { role: cbrunnkvist.ansistrano-symfony-deploy }

We will come back to this file with some extra tasks. Now, create an etc/deploy/group_vars folder which will contain the variables for each hosts. So, create an etc/deploy/group_vars/prod file with the following lines:

---
deploy_to: /path/to/your/deployment/directory
symfony_php_path: php

You have finished! Let’s deploy the Symfony application. Type the following command in a terminal:

$ ansible-playbook -i etc/ansible/prod etc/deploy/deploy.yml

You made it! That was great, but, let’s add some more features. What about your gulp tasks?

Adding custom tasks

If you read Carlos Buenosvinos’ role, you will see something like this:

# Hooks: custom tasks if you need them
ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-before-setup-tasks.yml"
ansistrano_after_setup_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-after-setup-tasks.yml"
ansistrano_before_update_code_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-before-update-code-tasks.yml"
ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-after-update-code-tasks.yml"
ansistrano_before_symlink_shared_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-before-symlink-shared-tasks.yml"
ansistrano_after_symlink_shared_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-after-symlink-shared-tasks.yml"
ansistrano_before_symlink_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-before-symlink-tasks.yml"
ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-after-symlink-tasks.yml"
ansistrano_before_cleanup_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-before-cleanup-tasks.yml"
ansistrano_after_cleanup_tasks_file: "{{ playbook_dir }}/<your-deployment-config>/my-after-cleanup-tasks.yml"

We are able to hook the deployment steps. So, edit your deploy.yml file and add the following line that will allow us to launch our gulp tasks:

ansistrano_before_symlink_shared_tasks_file: "{{ playbook_dir }}/config/before-symlink-shared-tasks.yml"

And, of course, create a etc/deploy/config/before-symlink-shared-tasks-yml file with this content:

- include: "config/steps/gulp_{{ gulp_env }}.yml"

Hey, what the heck is gulp_env? You must define this variable, which will allow you to launch your gulp tasks locally or remotely. Edit your group_vars/prod file:

---
deploy_to: /path/to/your/deployment/directory
symfony_php_path: php
gulp_env: remote

gulp_env could be either remote or local. So, we must create two different files: gulp_remote.yml and gulp_local.yml, both inside an etc/deploy/config/steps folder. This is the remote version (when you have gulp installed on your servers):

---
- name: '[remote] npm dependencies & gulp tasks'
shell: chdir={{ ansistrano_release_path.stdout }}
yarn install && gulp prod

Easy. But, when doing this locally, you must run your gulp tasks and then upload the resulting files. This is a gulp_local.yml example file:

---
- name: '[local] npm dependencies & gulp tasks'
local_action: shell cd {{ playbook_dir }}/../.. && yarn install && gulp prod
register: gulp_local_task
ignore_errors: True

- fail: msg="yarn install or gulp prod failed locally {{ gulp.stdout }}"
when: gulp_local_task|failed

- name: Upload CSS compiled files
synchronize:
src:
"{{ playbook_dir }}/../../web/css"
dest: "{{ ansistrano_release_path.stdout }}/web"
when: gulp_local_task|succeeded

- name: Upload JS compiled files
synchronize:
src:
"{{ playbook_dir }}/../../web/js"
dest: "{{ ansistrano_release_path.stdout }}/web"
when: gulp_local_task|succeeded

That was amazing! Run your deployment again and see your gulp tasks running on your terminal.

Conclusion

After using Magallanes, Capistrano and Ansible, I prefer the last one just because managing Ruby gems is a pain in the ass. Even with bundler this becomes a serious business, and I’ve had lots of problems with that. Ansible is a little slower, but it also opens you a new world of automatization, full of examples and roles that are quite awesome (see Ansible Galaxy). Magallanes… ok, it’s a good proof of concept but I would never use it in a professional environment. Feel free to tell me if I’m wrong, that’s the way I learn.