We have a standard LEMP setup NGINX, PHP-FPM.
Application – Yii-framework, deployed from Jenkins using Ansible role with the synchronize
module on backend hosts in a /data/projects/prjectname/frontend/web
, directory which is set as a root for an NGINX virtual host.
The task is to have the ability to deploy the same application on the same backend but to have multiple branches deployed by Ansible and served by NGINX.
Let’s use map
in NGINX here: if a special header will be added during a GET-request – then NGINX has to return code from the /data/projects/prjectname/<BRANCHNAME>/frontend/web
, if no header was set – then use the default directory /data/projects/prjectname/frontend/web
.
Accordingly and deploy process needs to be updated – code must be placed to the /data/projects/prjectname/frontend/web
or into the /data/projects/prjectname/<BRANCHNAME>/frontend/web
– depending on conditions.
Contents
NGINX map
Update nginx.conf
:
... map $http_ci_branch $app_branch { default ""; ~(.+) $1; } ...
Check syntax:
[simterm]
root@bttrm-dev-app-1:/home/admin# nginx -t nginx: [emerg] unknown "1" variable nginx: configuration file /etc/nginx/nginx.conf test failed
[/simterm]
Check NGINX’s version:
[simterm]
root@bttrm-dev-app-1:/home/admin# nginx -v nginx version: nginx/1.10.3
[/simterm]
Update it.
Uninstall already installed NGINX:
[simterm]
root@bttrm-dev-app-1:/home/admin# apt -y purge nginx
[/simterm]
Add its official repository:
[simterm]
root@bttrm-dev-app-1:/home/admin# echo "deb http://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list root@bttrm-dev-app-1:/home/admin# wget http://nginx.org/keys/nginx_signing.key root@bttrm-dev-app-1:/home/admin# apt-key add nginx_signing.key OK root@bttrm-dev-app-1:/home/admin# apt update
[/simterm]
Install the latest version:
[simterm]
root@bttrm-dev-app-1:/home/admin# apt -y install nginx
[/simterm]
Check:
[simterm]
root@bttrm-dev-app-1:/home/admin# nginx -v nginx version: nginx/1.17.0 root@bttrm-dev-app-1:/home/admin# nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful root@bttrm-dev-app-1:/home/admin# systemctl start nginx
[/simterm]
Go back to its config.
In the nginx.conf
we have now:
... underscores_in_headers on; map $http_ci_branch $app_branch { default ""; ~(.+) $1; } ...
Here we are getting http_ci_branch
variable (which will be created from the ci_branch
header which will be passed during a request) and then saving its value to the app_branch
variable:
- default – if
ci_branch
is empty the saveapp_branch
with the “” value - otherwise, get the
ci_branch
‘s value using regex ((.+)
) and save it to theapp_branch
To be able to use underscores in headers names – enable NGINX’s “underscores_in_headers
” option.
Then, update a virtual host’s config – add $app_branch
to the virtual host’s root
:
... set $root_path /data/projects/projectname/$app_branch/frontend/web; root $root_path; ...
Check it.
Create a new directory, for example, “develop“:
[simterm]
root@bttrm-dev-app-1:/home/admin# mkdir -p /data/projects/projectname/develop/frontend/web/
[/simterm]
Now we have to catalogs on a host – /data/projects/projectname/frontend/web/
and /data/projects/projectname/develop/frontend/web/
.
The first must be used by NGINX if ci_branch
will be empty, and the second one – if ci_branch
will have “develop” value.
Files are almost identical
The default directory’s index file:
[simterm]
root@bttrm-dev-app-1:/etc/nginx# cat /data/projects/projectname/frontend/web/index.php Root <?php $headers = getallheaders(); foreach($headers as $key=>$val){ echo $key . ': ' . $val . '<br>'; } ?>
[/simterm]
And in the develop:
[simterm]
root@bttrm-dev-app-1:/etc/nginx# cat /data/projects/projectname/develop/frontend/web/index.php Develop <?php $headers = getallheaders(); foreach($headers as $key=>$val){ echo $key . ': ' . $val . '<br>'; } ?>
[/simterm]
Let’s check.
Without the ci_branch
header first:
[simterm]
$ curl https://dev.example.com/ Root Accept: */*<br>User-Agent: curl/7.65.1<br>X-Amzn-Trace-Id: Root=1-5d1205f9-66ee8ea4b58b3400e02ecac4<br>Host: dev.example.com<br>X-Forwarded-Port: 443<br>X-Forwarded-Proto: https<br>X-Forwarded-For: 194.183.169.27<br>Content-Length: <br>Content-Type: <br>
[/simterm]
And with it and its value as “develop“:
[simterm]
$ curl -H "ci_branch:develop" https://dev.example.com/ Develop Ci-Branch: develop<br>Accept: */*<br>User-Agent: curl/7.65.1<br>X-Amzn-Trace-Id: Root=1-5d120606-7e42cd0b419e20071d7f8a97<br>Host: dev.example.com<br>X-Forwarded-Port: 443<br>X-Forwarded-Proto: https<br>X-Forwarded-For: 194.183.169.27<br>Content-Length: <br>Content-Type: <br>
[/simterm]
Great – all works here.
Ansible deploy
Now we can serve different content with the header passed and it’s time to update deployment – to copy the code to a specific (or default) directories on hosts.
The deployment’s role main task now looks like next:
... - name: "Deploy application to the {{ aws_env }} environment hosts" synchronize: src: "app/" dest: "/data/projects/{{ backend_project_name }}" use_ssh_args: true delete: true rsync_opts: - "--exclude=uploads" ...
backend_project_name
variable passed to the role from a playbook’s file:
... - role: deploy tags: deploy backend_prodject_git_branch: "{{ lookup('env','APP_REPO_BRANCH') }}" backend_project_git_repo: "{{ lookup('env','APP_REPO_RUL') }}" backend_project_name: "{{ lookup('env','APP_PROJECT_NAME') }}" when: "'backend-bastion' not in inventory_hostname"
To make new deployment working need to add one more directory in the dest: "/data/projects/{{ backend_project_name }}"
with the next conditions:
- must be applied only for Dev or Staging environments
- apply only if a branch’s name != develop, as develop is the default branch for Dev and Staging, and code from this branch must be deployed to the default directory
/data/projects/{{ backend_project_name }}
Add set_fact
in the role’s playbook:
... - set_fact: backend_branch: "{{ lookup('env','APP_REPO_BRANCH') }}" when: "'develop' not in lookup('env','APP_REPO_BRANCH') and 'production' not in env" ...
But now if this role will be used from a Production or with the develop branch – the backend_branch
variable will not be set at all.
Let’s add some default value here in the group_vars/all.yml
file:
... backend_branch: "" ...
Thus, it will be set with the “” value first but with the lowest priority (see Ansible’s priorities here>>>), and then, if the when
condition in the set_fact
will be applied – it will overwrite the `backend_branch` variable’s value.
Update deployment role add the {{ backend_branch }}
variable and a couple debug messages:
... - set_fact: backend_branch: "{{ lookup('env','APP_REPO_BRANCH') }}" when: "'develop' not in lookup('env','APP_REPO_BRANCH') and 'production' not in env" - name: Test task debug: msg: "Backend branch: {{ backend_branch }}" - name: Test task debug: msg: "Deploy dir: {{ web_data_root_prefix }}/{{ backend_project_name }}/{{ backend_branch }}" - meta: end_play ...
Check it – add an environment’s variable APP_REPO_BRANCH="blabla"
and an application’s name variable, used in the deploy
role:
[simterm]
$ export APP_PROJECT_NAME=projectname $ export APP_REPO_BRANCH="blabla"
[/simterm]
Run the script:
[simterm]
$ ./ansible_exec.sh -t deploy ... TASK [deploy : Test task] **** ok: [dev.backend-app1-internal.example.com] => { "msg": "Backend branch: blabla" } ok: [dev.backend-app2-internal.example.com] => { "msg": "Backend branch: blabla" } ok: [dev.backend-console-internal.example.com] => { "msg": "Backend branch: blabla" } skipping: [dev.backend-bastion.example.com] TASK [deploy : Test task] **** ok: [dev.backend-app1-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/blabla" } ok: [dev.backend-app2-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/blabla" } ok: [dev.backend-console-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/blabla" } ...
[/simterm]
Okay.
Now change the branch’s variable value to the develop:
[simterm]
$ export APP_REPO_BRANCH="develop" $ ./ansible_exec.sh -t deploy ... TASK [deploy : Test task] **** ok: [dev.backend-app1-internal.example.com] => { "msg": "Backend branch: " } ok: [dev.backend-app2-internal.example.com] => { "msg": "Backend branch: " } ok: [dev.backend-console-internal.example.com] => { "msg": "Backend branch: " } skipping: [dev.backend-bastion.example.com] TASK [deploy : Test task] **** ok: [dev.backend-app1-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/" } ok: [dev.backend-app2-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/" } ok: [dev.backend-console-internal.example.com] => { "msg": "Deploy dir: /data/projects/projectname/" } ...
[/simterm]
Looks good?
Update deployment tasks – add {{ backend_branch }}
to the destination path:
... - name: "Deploy application to the {{ aws_env }} environment hosts" synchronize: src: "app/" dest: "/data/projects/{{ backend_project_name }}/{{ backend_branch }}" use_ssh_args: true delete: true rsync_opts: - "--exclude=uploads" ...
Deploy from Jenkins, application’s branch used here – sentinel-cache-client:
Check directories on a backend host:
[simterm]
root@bttrm-dev-app-1:/etc/nginx# ll /data/projects/projectname/ total 1380 drwxr-xr-x 14 projectname projectname 4096 Jun 13 17:19 backend -rw-r--r-- 1 projectname projectname 167 Jun 13 17:19 codeception.yml ... -rw-r--r-- 1 projectname projectname 5050 Jun 13 17:19 requirements.php drwxr-xr-x 11 projectname projectname 4096 Jun 25 17:29 sentinel-cache-client drwxr-xr-x 3 projectname projectname 4096 Jun 13 17:22 storage -rw-r--r-- 1 root root 5 Jun 25 17:29 test.txt -rw-r--r-- 1 projectname projectname 2624 Jun 13 17:19 Vagrantfile ...
[/simterm]
The sentinel-cache-client directory created – nice.
And it has the same content as the default directory, just with the sentinel-cache-client branch:
[simterm]
root@bttrm-dev-app-1:/etc/nginx# ll /data/projects/projectname/sentinel-cache-client/ total 1380 drwxr-xr-x 14 projectname projectname 4096 Jun 13 17:19 backend -rw-r--r-- 1 projectname projectname 167 Jun 13 17:19 codeception.yml drwxr-xr-x 13 projectname projectname 4096 Jun 13 17:19 common -rw-r--r-- 1 projectname projectname 2551 Jun 13 17:19 composer.json -rw-r--r-- 1 projectname projectname 222276 Jun 13 17:19 composer.lock drwxr-xr-x 9 projectname projectname 4096 Jun 13 17:19 console drwxr-xr-x 6 projectname projectname 4096 Jun 13 17:19 docker ...
[/simterm]
Check it.
Create a test file in the /data/projects/projectname/sentinel-cache-client/
directory:
[simterm]
root@bttrm-dev-app-1:/etc/nginx# echo "sentinel-cache-client" > /data/projects/projectname/sentinel-cache-client/frontend/web/test.php
[/simterm]
Now call URL without header:
[simterm]
$ curl https://dev.example.com.com/test.php <html> <head><title>404 Not Found</title></head> ...
[/simterm]
And with the ci_branch
header and “sentinel-cache-client” as its value:
[simterm]
$ curl -H "ci_branch:sentinel-cache-client" https://dev.example.com.com/test.php sentinel-cache-client
[/simterm]
All works.
Done.