NGINX: multi-branch deployment with Ansible, NGINX map and HTTP Headers
Tags: ansible, jenkins, nginx, deployment
Published: 2019-06-26 12:01:38

---
title: NGINX: multi-branch deployment with Ansible, NGINX map and HTTP Headers
published: true
tags: Ansible,Jenkins,NGINX,deployment
canonical_url: https://rtfm.co.ua/en/nginx-multi-branch-deployment-with-ansible-nginx-map-and-http-headers/
---

![](https://rtfm.co.ua/wp-content/uploads/2012/09/nginx_logo2.jpg)We have a standard LEMP setup NGINX, PHP-FPM.

Application – Yii-framework, deployed from Jenkins using Ansible role with the [`synchronize`](https://docs.ansible.com/ansible/latest/modules/synchronize_module.html) 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`](http://nginx.org/en/docs/http/ngx_http_map_module.html) 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;
    }
...
```

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
```

Check NGINX’s version:

```
root@bttrm-dev-app-1:/home/admin# nginx -v
nginx version: nginx/1.10.3
```

Update it.

Uninstall already installed NGINX:

```
root@bttrm-dev-app-1:/home/admin# apt -y purge nginx
```

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
```

Install the latest version:

```
root@bttrm-dev-app-1:/home/admin# apt -y install nginx
```

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
```

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 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;
...
```

Check it.

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

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

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

}

?>
```

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

}

?>
```

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>
```

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>
```

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`](https://docs.ansible.com/ansible/latest/modules/set_fact_module.html) 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>>>](https://gist.github.com/ekreutz/301c3d38a50abbaad38e638d8361a89e)), 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:

```
$ export APP_PROJECT_NAME=projectname
$ export APP_REPO_BRANCH="blabla"
```

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"
}
...
```

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/"
}
...
```

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_:

[![](https://rtfm.co.ua/wp-content/uploads/2019/06/Screenshot_20190625_173225.png "NGINX: multi-branch deployment with Ansible, NGINX map and HTTP Headers")](https://rtfm.co.ua/wp-content/uploads/2019/06/Screenshot_20190625_173225.png)

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
...
```

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
...
```

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
```

Now call URL without header:

```
$ curl https://dev.example.com.com/test.php
<html>
<head><title>404 Not Found</title></head>
...
```

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
```

All works.

Done.

### Similar posts
 
- <small>03/15/2019</small> [Jenkins: HTTP full-duplex channel timeout](https://rtfm.co.ua/jenkins-http-full-duplex-channel-timeout/)<small> (0)</small>  
- <small>09/20/2017</small> [Ansible: пример установки NGINX](https://rtfm.co.ua/ansible-primer-ustanovki-nginx/)<small> (0)</small>  
- <small>03/15/2019</small> [Jenkins: HTTP full-duplex channel timeout](https://dev.to/setevoy/jenkins-http-full-duplex-channel-timeout-3hch-temp-slug-4867599)<small> (0)</small>  
- <small>08/25/2018</small> [AWS: миграция RTFM 3.0 (final) – CloudFormation и Ansible роли](https://rtfm.co.ua/aws-migraciya-rtfm-3-0-final-cloudformation-i-ansible-roli/)<small> (0)</small>