NGINX: мульти-бранч деплой приложения с использованием NGINX map и HTTP Headers

Автор: | 26/06/2019

Имеется стандартный LEMP – NGINX, PHP-FPM.

Приложение – Yii-фреймворк, который деплоится из Jenkins Ansible-ролью с помощью модуля synchronize на хосты в каталог /data/projects/prjectname/frontend/web, который является root в конфиге виртуалхоста NGINX.

Задача: создать возможность деплоя из Jenkins приложения из разных бранчей – на хосте одновременно должны быть задеплоены разные версии приложения, доступ к которым будет определяться через специальный заголовок запросов.

Попробуем реализовать через map: при передаче в запросе заголовка с именем бранча – NGINX должен отдавать root вида /data/projects/prjectname/<BRANCHNAME>/frontend/web, если заголовка нет – то отдаём из /data/projects/prjectname/frontend/web.

Соответственно, и деплой должен выполнятся либо в /data/projects/prjectname/frontend/web, либо в каталог /data/projects/prjectname/<BRANCHNAME>/frontend/web.

Содержание

NGINX map

Обновляем nginx.conf:

...
    map $http_ci_branch $app_branch {
        default "";
        ~(.+)   $1;
    }
...

Проверяем синтаксис:

[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]

Проверяем версию NGINX:

[simterm]

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

[/simterm]

Обновляем версию.

Удаляем установленный NGINX:

[simterm]

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

[/simterm]

Добавляем официальный репозиторий:

[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]

Устанавливаем из него последнюю версию NGINX:

[simterm]

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

[/simterm]

Проверяем:

[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]

Возвращаемся к конфигу.

В nginx.conf мы сейчас имеем:

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

Тут мы получаем переменную http_ci_branch (которая формируется из заголовка ci_branch, который мы передаём в запросе), и её значение сохраняем в переменной app_branch:

  • default – если в ci_branch не передано ничего, то в app_branch сохраняем “”
  • иначе получаем значение ci_branch ((.+)), и записываем его в app_branch

Для использования символа “_” в именах заголовков – включаем их поддержку с помощью “underscores_in_headers on;“.

Далее, обновляем конфиг виртуалхоста – добавляем подстановку значения переменной $app_branch в root виртуалхоста:

...
    set $root_path /data/projects/projectname/$app_branch/frontend/web;
    root $root_path;
...

Проверяем.

Создаём второй каталог – “develop“:

[simterm]

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

[/simterm]

Теперь у нас есть два каталога – /data/projects/projectname/frontend/web/ и /data/projects/projectname/develop/frontend/web/.

Первый отдаём, если в ci_branch не будет ничего, а второй – в случае, если в ci_branch приходит значение develop.

Содержимое файлов практически одинаковое.

Корневой каталог:

[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]

И каталог для 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]

Проверяем.

Сначала без заголовка:

[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]

И с заголовком:

[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]

Окей – тут всё работает.

Ansible deploy

Что дальше?

Отдавать данные из разных каталогов с помощью NGINX получилось – теперь надо обновить деплой, что бы загружать данные в правильные каталоги.

Сейчас основная задача роли deploy в Ansible выглядит так:

...
- 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 передаётся в роль из плейбука:

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

Что бы новая схема заработала – надо в dest: "/data/projects/{{ backend_project_name }}" добавить ещё один каталог, но с условиями:

  • применять его только на Dev или Staging окружении
  • применять только если имя бранча != develop, т.к. develop является default-бранчём для Dev и Staging, и из develop-бранча код должен деплоиться напрямую в корень /data/projects/{{ backend_project_name }}

Добавляем set_fact в плейбук роли:

...
- set_fact: 
    backend_branch: "{{ lookup('env','APP_REPO_BRANCH') }}"
  when: "'develop' not in lookup('env','APP_REPO_BRANCH') and 'production' not in env"
...

Но в таком случае, если роль будет вызвана на Production или с бранчём develop – переменная backend_branch не будет задана вообще.

Задаём ей дефолтное значение “” – обновляем group_vars/all.yml:

...
backend_branch: ""
...

Теперь она будет сначала задана с наименьшим приоритетом со значением “” (см. приоритеты тут>>>), а затем, если условие when в самой задаче с set_fact сработает – то оно перезапишет значение с реальным именем бранча.

Обновляем таски деплоя – добавляем обновление переменной {{ backend_branch }} и проверку её значения:

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

Проверяем – задачём переменную APP_REPO_BRANCH="blabla" и переменную с именем приложения:

[simterm]

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

[/simterm]

Запускаем скрипт:

[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]

Окей.

Теперь меняем бранч на 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]

Вроде работает?

Обновляем таску деплоя, добавляем {{ backend_branch }} к пути:

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

Деплоим из Jenkins, бранч приложения – sentinel-cache-client:

Проверяем каталоги на сервере:

[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]

Каталог sentinel-cache-client появился, отлично.

И в нём – тоже содержимое, только из бранча sentinel-cache-client:

[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]

Проверим.

В каталоге /data/projects/projectname/sentinel-cache-client/ создаём тестовый файлик:

[simterm]

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

[/simterm]

Проверяем без заголовка:

[simterm]

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

[/simterm]

И с заголовком бранча:

[simterm]

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

[/simterm]

Всё работает.

Готово.