NGINX: multi-branch deployment with Ansible, NGINX map and HTTP Headers

setevoy

Arseny Zinchenko

Posted on June 26, 2019

NGINX: multi-branch deployment with Ansible, NGINX map and HTTP Headers

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.

NGINX map

Update nginx.conf:

...
    map $http_ci_branch $app_branch {
        default "";
        ~(.+)   $1;
    }
...
Enter fullscreen mode Exit fullscreen mode

Check syntax:

root@bttrm-dev-app-1:/home/admin# nginx -t
nginx: [emerg] unknown "1" variable
nginx: configuration file /etc/nginx/nginx.conf test failed
Enter fullscreen mode Exit fullscreen mode

Check NGINX’s version:

root@bttrm-dev-app-1:/home/admin# nginx -v
nginx version: nginx/1.10.3
Enter fullscreen mode Exit fullscreen mode

Update it.

Uninstall already installed NGINX:

root@bttrm-dev-app-1:/home/admin# apt -y purge nginx
Enter fullscreen mode Exit fullscreen mode

Add its official repository:

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
Enter fullscreen mode Exit fullscreen mode

Install the latest version:

root@bttrm-dev-app-1:/home/admin# apt -y install nginx
Enter fullscreen mode Exit fullscreen mode

Check:

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
Enter fullscreen mode Exit fullscreen mode

Go back to its config.

In the nginx.conf we have now:

...
    underscores_in_headers on;
    map $http_ci_branch $app_branch {
        default "";
        ~(.+)   $1;
    }
...
Enter fullscreen mode Exit fullscreen mode

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 save app_branch with the “” value
  • otherwise, get the ci_branch‘s value using regex ((.+)) and save it to the app_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;
...
Enter fullscreen mode Exit fullscreen mode

Check it.

Create a new directory, for example, “develop“:

root@bttrm-dev-app-1:/home/admin# mkdir -p /data/projects/projectname/develop/frontend/web/
Enter fullscreen mode Exit fullscreen mode

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:

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>';

}

?>
Enter fullscreen mode Exit fullscreen mode

And in the develop:

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>';

}

?>
Enter fullscreen mode Exit fullscreen mode

Let’s check.

Without the ci_branch header first:

$ 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>
Enter fullscreen mode Exit fullscreen mode

And with it and its value as “develop“:

$ 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>
Enter fullscreen mode Exit fullscreen mode

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"
...
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
...
Enter fullscreen mode Exit fullscreen mode

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: ""
...
Enter fullscreen mode Exit fullscreen mode

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 
...
Enter fullscreen mode Exit fullscreen mode

Check it – add an environment’s variable APP_REPO_BRANCH="blabla" and an application’s name variable, used in the deploy role:

$ export APP_PROJECT_NAME=projectname
$ export APP_REPO_BRANCH="blabla"
Enter fullscreen mode Exit fullscreen mode

Run the script:

$ ./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"
}
...
Enter fullscreen mode Exit fullscreen mode

Okay.

Now change the branch’s variable value to the develop:

$ 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/"
}
...
Enter fullscreen mode Exit fullscreen mode

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"
...
Enter fullscreen mode Exit fullscreen mode

Deploy from Jenkins, application’s branch used here – sentinel-cache-client:

Check directories on a backend host:

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
...
Enter fullscreen mode Exit fullscreen mode

The sentinel-cache-client directory created – nice.

And it has the same content as the default directory, just with the sentinel-cache-client branch:

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
...
Enter fullscreen mode Exit fullscreen mode

Check it.

Create a test file in the /data/projects/projectname/sentinel-cache-client/ directory:

root@bttrm-dev-app-1:/etc/nginx# echo "sentinel-cache-client" > /data/projects/projectname/sentinel-cache-client/frontend/web/test.php
Enter fullscreen mode Exit fullscreen mode

Now call URL without header:

$ curl https://dev.example.com.com/test.php
<html>
<head><title>404 Not Found</title></head>
...
Enter fullscreen mode Exit fullscreen mode

And with the ci_branch header and “sentinel-cache-client” as its value:

$ curl -H "ci_branch:sentinel-cache-client" https://dev.example.com.com/test.php
sentinel-cache-client
Enter fullscreen mode Exit fullscreen mode

All works.

Done.

Similar posts

💖 💪 🙅 🚩
setevoy
Arseny Zinchenko

Posted on June 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related