Ansible Deployments with Ansistrano¶
In this chapter you will learn how to deploy applications with the Ansible role Ansistrano.
Objectives: In this chapter you will learn how to:
Implement Ansistrano;
Configure Ansistrano;
Use shared folders and files between deployed versions;
Deploying different versions of a site from git;
React between deployment steps.
ansible, ansistrano, roles, deployments
Knowledge:
Complexity:
Reading time: 40 minutes
Ansistrano is an Ansible role to easily deploy PHP, Python, etc. applications. It is based on the functionality of Capistrano.
Introduction¶
Ansistrano requires the following to run:
- Ansible on the deployment machine,
rsyncorgiton the client machine.
It can download source code from rsync, git, scp, http, S3, ...
Note
For our deployment example, we will use the git protocol.
Ansistrano deploys applications by following these 5 steps:
- Setup: create the directory structure to host the releases;
- Update Code: downloading the new release to the targets;
- Symlink Shared and Symlink: after deploying the new release, the
currentsymbolic link is modified to point to this new release; - Clean Up: to do some clean up (remove old versions).

The skeleton of a deployment with Ansistrano looks like this:
/var/www/site/
├── current -> ./releases/20210718100000Z
├── releases
│ └── 20210718100000Z
│ ├── css -> ../../shared/css/
│ ├── img -> ../../shared/img/
│ └── REVISION
├── repo
└── shared
├── css/
└── img/
You can find all the Ansistrano documentation on its Github repository.
Labs¶
You will continue to work on your 2 servers:
The management server:
- Ansible is already installed. You will have to install the
ansistrano.deployrole.
The managed server:
- You will need to install Apache and deploy the client site.
Deploying the Web server¶
For more efficiency, we will use the geerlingguy.apache role to configure the server:
$ ansible-galaxy role install geerlingguy.apache
Starting galaxy role install process
- downloading role 'apache', owned by geerlingguy
- downloading role from https://github.com/geerlingguy/ansible-role-apache/archive/3.1.4.tar.gz
- extracting geerlingguy.apache to /home/ansible/.ansible/roles/geerlingguy.apache
- geerlingguy.apache (3.1.4) was installed successfully
We will probably need to open some firewall rules, so we will also install the collection ansible.posix to work with its module firewalld:
$ ansible-galaxy collection install ansible.posix
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/download/ansible-posix-1.2.0.tar.gz to /home/ansible/.ansible/tmp/ansible-local-519039bp65pwn/tmpsvuj1fw5/ansible-posix-1.2.0-bhjbfdpw
Installing 'ansible.posix:1.2.0' to '/home/ansible/.ansible/collections/ansible_collections/ansible/posix'
ansible.posix:1.2.0 was installed successfully
Once the role and the collection are installed, we can create the first part of our playbook, which will:
- Install Apache,
- Create a target folder for our
vhost, - Create a default
vhost, - Open the firewall,
- Start or restart Apache.
Technical considerations:
- We will deploy our site to the
/var/www/site/folder. - As we will see later,
ansistranowill create acurrentsymbolic link to the current release folder. - The source code to be deployed contains a
htmlfolder which the vhost should point to. ItsDirectoryIndexisindex.htm. - The deployment is done by
git, the package will be installed.
Note
The target of our vhost will therefore be: /var/www/site/current/html.
Our playbook to configure the server: playbook-config-server.yml
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
apache_global_vhost_settings: |
DirectoryIndex index.php index.htm
apache_vhosts:
- servername: "website"
documentroot: "{{ dest }}current/html"
tasks:
- name: create directory for website
file:
path: /var/www/site/
state: directory
mode: 0755
- name: install git
package:
name: git
state: latest
- name: permit traffic in default zone for http service
ansible.posix.firewalld:
service: http
permanent: yes
state: enabled
immediate: yes
roles:
- { role: geerlingguy.apache }
The playbook can be applied to the server:
ansible-playbook playbook-config-server.yml
Note the execution of the following tasks:
TASK [geerlingguy.apache : Ensure Apache is installed on RHEL.] ****************
TASK [geerlingguy.apache : Configure Apache.] **********************************
TASK [geerlingguy.apache : Add apache vhosts configuration.] *******************
TASK [geerlingguy.apache : Ensure Apache has selected state and enabled on boot.] ***
TASK [permit traffic in default zone for http service] *************************
RUNNING HANDLER [geerlingguy.apache : restart apache] **************************
The geerlingguy.apache role makes our job much easier by taking care of the installation and configuration of Apache.
You can check that everything is working by using curl:
$ curl -I http://192.168.1.11
HTTP/1.1 404 Not Found
Date: Mon, 05 Jul 2021 23:30:02 GMT
Server: Apache/2.4.37 (rocky) OpenSSL/1.1.1g
Content-Type: text/html; charset=iso-8859-1
Note
We have not yet deployed any code, so it is normal for curl to return a 404 HTTP code. But we can already confirm that the httpd service is working and that the firewall is open.
Deploying the software¶
Now that our server is configured, we can deploy the application.
For this, we will use the ansistrano.deploy role in a second playbook dedicated to application deployment (for more readability).
$ ansible-galaxy role install ansistrano.deploy
Starting galaxy role install process
- downloading role 'deploy', owned by ansistrano
- downloading role from https://github.com/ansistrano/deploy/archive/3.10.0.tar.gz
- extracting ansistrano.deploy to /home/ansible/.ansible/roles/ansistrano.deploy
- ansistrano.deploy (3.10.0) was installed successfully
The sources of the software can be found in the github repository.
We will create a playbook playbook-deploy.yml to manage our deployment:
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
roles:
- { role: ansistrano.deploy }
$ ansible-playbook playbook-deploy.yml
PLAY [ansible_clients] *********************************************************
TASK [ansistrano.deploy : ANSISTRANO | Ensure deployment base path exists] *****
TASK [ansistrano.deploy : ANSISTRANO | Ensure releases folder exists]
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared elements folder exists]
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared paths exists]
TASK [ansistrano.deploy : ANSISTRANO | Ensure basedir shared files exists]
TASK [ansistrano.deploy : ANSISTRANO | Get release version] ********************
TASK [ansistrano.deploy : ANSISTRANO | Get release path]
TASK [ansistrano.deploy : ANSISTRANO | GIT | Register ansistrano_git_result variable]
TASK [ansistrano.deploy : ANSISTRANO | GIT | Set git_real_repo_tree]
TASK [ansistrano.deploy : ANSISTRANO | GIT | Create release folder]
TASK [ansistrano.deploy : ANSISTRANO | GIT | Sync repo subtree[""] to release path]
TASK [ansistrano.deploy : ANSISTRANO | Copy git released version into REVISION file]
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared paths targets are absent]
TASK [ansistrano.deploy : ANSISTRANO | Create softlinks for shared paths and files]
TASK [ansistrano.deploy : ANSISTRANO | Ensure .rsync-filter is absent]
TASK [ansistrano.deploy : ANSISTRANO | Setup .rsync-filter with shared-folders]
TASK [ansistrano.deploy : ANSISTRANO | Get current folder]
TASK [ansistrano.deploy : ANSISTRANO | Remove current folder if it's a directory]
TASK [ansistrano.deploy : ANSISTRANO | Change softlink to new release]
TASK [ansistrano.deploy : ANSISTRANO | Clean up releases]
PLAY RECAP ********************************************************************************************************************************************************************************************************
192.168.1.11 : ok=25 changed=8 unreachable=0 failed=0 skipped=14 rescued=0 ignored=0
So many things done with only 11 lines of code!
$ curl http://192.168.1.11
<html>
<head>
<title>Demo Ansible</title>
</head>
<body>
<h1>Version Master</h1>
</body>
<html>
Checking on the server¶
You can now connect by ssh to your client machine.
- Make a
treeon the/var/www/site/directory:
$ tree /var/www/site/
/var/www/site
├── current -> ./releases/20210722155312Z
├── releases
│ └── 20210722155312Z
│ ├── REVISION
│ └── html
│ └── index.htm
├── repo
│ └── html
│ └── index.htm
└── shared
Please note:
- the
currentsymlink to the release./releases/20210722155312Z - the presence of a directory
shared -
the presence of the git repos in
./repo/ -
From the Ansible server, restart the deployment 3 times, then check on the client.
$ tree /var/www/site/
var/www/site
├── current -> ./releases/20210722160048Z
├── releases
│ ├── 20210722155312Z
│ │ ├── REVISION
│ │ └── html
│ │ └── index.htm
│ ├── 20210722160032Z
│ │ ├── REVISION
│ │ └── html
│ │ └── index.htm
│ ├── 20210722160040Z
│ │ ├── REVISION
│ │ └── html
│ │ └── index.htm
│ └── 20210722160048Z
│ ├── REVISION
│ └── html
│ └── index.htm
├── repo
│ └── html
│ └── index.htm
└── shared
Please note:
ansistranokept the 4 last releases,- the
currentlink linked now to the lastest release
Limit the number of releases¶
The ansistrano_keep_releases variable is used to specify the number of releases to keep.
- Using the
ansistrano_keep_releasesvariable, keep only 3 releases of the project. Check.
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
roles:
- { role: ansistrano.deploy }
---
$ ansible-playbook -i hosts playbook-deploy.yml
On the client machine:
$ tree /var/www/site/
/var/www/site
├── current -> ./releases/20210722160318Z
├── releases
│ ├── 20210722160040Z
│ │ ├── REVISION
│ │ └── html
│ │ └── index.htm
│ ├── 20210722160048Z
│ │ ├── REVISION
│ │ └── html
│ │ └── index.htm
│ └── 20210722160318Z
│ ├── REVISION
│ └── html
│ └── index.htm
├── repo
│ └── html
│ └── index.htm
└── shared
Using shared_paths and shared_files¶
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "logs"
roles:
- { role: ansistrano.deploy }
On the client machine, create the file logs in the shared directory:
sudo touch /var/www/site/shared/logs
Then execute the playbook:
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared paths targets are absent] *******************************************************
ok: [192.168.10.11] => (item=img)
ok: [192.168.10.11] => (item=css)
ok: [192.168.10.11] => (item=logs/log)
TASK [ansistrano.deploy : ANSISTRANO | Create softlinks for shared paths and files] **************************************************
changed: [192.168.10.11] => (item=img)
changed: [192.168.10.11] => (item=css)
changed: [192.168.10.11] => (item=logs)
On the client machine:
$ tree -F /var/www/site/
/var/www/site/
├── current -> ./releases/20210722160631Z/
├── releases/
│ ├── 20210722160048Z/
│ │ ├── REVISION
│ │ └── html/
│ │ └── index.htm
│ ├── 20210722160318Z/
│ │ ├── REVISION
│ │ └── html/
│ │ └── index.htm
│ └── 20210722160631Z/
│ ├── REVISION
│ ├── css -> ../../shared/css/
│ ├── html/
│ │ └── index.htm
│ ├── img -> ../../shared/img/
│ └── logs -> ../../shared/logs
├── repo/
│ └── html/
│ └── index.htm
└── shared/
├── css/
├── img/
└── logs
Please note that the last release contains 3 links: css, img, and logs
- from
/var/www/site/releases/cssto the../../shared/css/directory. - from
/var/www/site/releases/imgto the../../shared/img/directory. - from
/var/www/site/releases/logsto the../../shared/logsfile.
Therefore, the files contained in these 2 folders and the logs file are always accessible via the following paths:
/var/www/site/current/css/,/var/www/site/current/img/,/var/www/site/current/logs,
but above all they will be kept from one release to the next.
Use a sub-directory of the repository for deployment¶
In our case, the repository contains a html folder, which contains the site files.
- To avoid this extra level of directory, use the
ansistrano_git_repo_treevariable by specifying the path of the sub-directory to use.
Don't forget to modify the Apache configuration to take into account this change!
Change the playbook for the server configuration playbook-config-server.yml
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
apache_global_vhost_settings: |
DirectoryIndex index.php index.htm
apache_vhosts:
- servername: "website"
documentroot: "{{ dest }}current/" # <1>
tasks:
- name: create directory for website
file:
path: /var/www/site/
state: directory
mode: 0755
- name: install git
package:
name: git
state: latest
roles:
- { role: geerlingguy.apache }
<1> Modify this line
Change the playbook for the deployment playbook-deploy.yml
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "log"
ansistrano_git_repo_tree: 'html' # <1>
roles:
- { role: ansistrano.deploy }
<1> Modify this line
-
Don't forget to run both of the playbooks
-
Check on the client machine:
$ tree -F /var/www/site/
/var/www/site/
├── current -> ./releases/20210722161542Z/
├── releases/
│ ├── 20210722160318Z/
│ │ ├── REVISION
│ │ └── html/
│ │ └── index.htm
│ ├── 20210722160631Z/
│ │ ├── REVISION
│ │ ├── css -> ../../shared/css/
│ │ ├── html/
│ │ │ └── index.htm
│ │ ├── img -> ../../shared/img/
│ │ └── logs -> ../../shared/logs
│ └── 20210722161542Z/
│ ├── REVISION
│ ├── css -> ../../shared/css/
│ ├── img -> ../../shared/img/
│ ├── index.htm
│ └── logs -> ../../shared/logs
├── repo/
│ └── html/
│ └── index.htm
└── shared/
├── css/
├── img/
└── logs
<1> Please note the absence of html
Managing git branch or tags¶
The ansistrano_git_branch variable is used to specify a branch or tag to deploy.
- Deploy the
releases/v1.1.0branch:
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "log"
ansistrano_git_repo_tree: 'html'
ansistrano_git_branch: 'releases/v1.1.0'
roles:
- { role: ansistrano.deploy }
Note
You can have fun, during the deployment, refreshing your browser, to see in 'live' the change.
$ curl http://192.168.1.11
<html>
<head>
<title>Demo Ansible</title>
</head>
<body>
<h1>Version 1.0.1</h1>
</body>
<html>
- Deploy the
v2.0.0tag:
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "log"
ansistrano_git_repo_tree: 'html'
ansistrano_git_branch: 'v2.0.0'
roles:
- { role: ansistrano.deploy }
$ curl http://192.168.1.11
<html>
<head>
<title>Demo Ansible</title>
</head>
<body>
<h1>Version 2.0.0</h1>
</body>
<html>
Actions between deployment steps¶
A deployment with Ansistrano respects the following steps:
SetupUpdate CodeSymlink SharedSymlinkClean Up
It is possible to intervene before and after each of these steps.

A playbook can be included through the variables provided for this purpose:
ansistrano_before_<task>_tasks_file-
or
ansistrano_after_<task>_tasks_file -
Easy example: send an email (or whatever you want like Slack notification) at the beginning of the deployment:
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "logs"
ansistrano_git_repo_tree: 'html'
ansistrano_git_branch: 'v2.0.0'
ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/deploy/before-setup-tasks.yml"
roles:
- { role: ansistrano.deploy }
Create the file deploy/before-setup-tasks.yml:
---
- name: Send a mail
mail:
subject: Starting deployment on {{ ansible_hostname }}.
delegate_to: localhost
TASK [ansistrano.deploy : include] *************************************************************************************
included: /home/ansible/deploy/before-setup-tasks.yml for 192.168.10.11
TASK [ansistrano.deploy : Send a mail] *************************************************************************************
ok: [192.168.10.11 -> localhost]
[root] # mailx
Heirloom Mail version 12.5 7/5/10. Type ? for help.
"/var/spool/mail/root": 1 message 1 new
>N 1 root@localhost.local Tue Aug 21 14:41 28/946 "Starting deployment on localhost."
- You will probably have to restart some services at the end of the deployment, to flush caches for example. Let's restart Apache at the end of the deployment:
---
- hosts: ansible_clients
become: yes
become_user: root
vars:
dest: "/var/www/site/"
ansistrano_deploy_via: "git"
ansistrano_git_repo: https://github.com/alemorvan/demo-ansible.git
ansistrano_deploy_to: "{{ dest }}"
ansistrano_keep_releases: 3
ansistrano_shared_paths:
- "img"
- "css"
ansistrano_shared_files:
- "logs"
ansistrano_git_repo_tree: 'html'
ansistrano_git_branch: 'v2.0.0'
ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/deploy/before-setup-tasks.yml"
ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/deploy/after-symlink-tasks.yml"
roles:
- { role: ansistrano.deploy }
Create the file deploy/after-symlink-tasks.yml:
---
- name: restart apache
systemd:
name: httpd
state: restarted
TASK [ansistrano.deploy : include] *************************************************************************************
included: /home/ansible/deploy/after-symlink-tasks.yml for 192.168.10.11
TASK [ansistrano.deploy : restart apache] **************************************************************************************
changed: [192.168.10.11]
As you have seen during this chapter, Ansible can greatly improve the life of the system administrator. Very intelligent roles like Ansistrano are "must haves" that quickly become indispensable.
Using Ansistrano, ensures that good deployment practices are respected, reduces the time needed to put a system into production, and avoids the risk of potential human errors. The machine works fast, well, and rarely makes mistakes!