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

Автор: | 06/26/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;
    }
...

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

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

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

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

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

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

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

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

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

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

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

Проверяем:

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

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

В 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«:

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

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

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

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

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

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

И каталог для 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>';
}
?>

Проверяем.

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

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>

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

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>

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

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" и переменную с именем приложения:

export APP_PROJECT_NAME=projectname
export APP_REPO_BRANCH="blabla"

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

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

Окей.

Теперь меняем бранч на 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/"
}
...

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

Обновляем таску деплоя, добавляем {{ 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:

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

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

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

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

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

Проверим.

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

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

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

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

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

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

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

Готово.