Примерно полгода назад, пришлось разработать схему обратного проксирования сайтов, с многих нод (n>20) на несколько (n<=3) боевых серверов. Недавно столкнулся в аналогичным запросом от коллеги. Поэтому решил поделиться, и все собрать в статью.

Уровень статьи — для начинающих.

Как результат, был необходим простой инструмент для добавления новых нод и обновления перечня доменов. Профит от такого решения должен быть, при использовании кеширования на сервере, и DNS с геолокацией.

Поиск информации по теме reverse-proxy, часто сводится к статьям по настройке “nginx to apache” (на локальный apache или на удаленный upstream-сервер), CDN-прокси сервисов (cloudflare, *cdn, cloudfront, etc.). В данном случае это не совсем подходило.
Особенность заключается в необходимости предоставлять множество разных IP (из различных географических локаций) для доменов одного-двух серверов.

Для решения задачи были куплены несколько VPS в необходимых различных локациях (дешевые, спасибо lowendbox.com & lowendstock.com, но с необходимым бендвичем). VPS пока использовались на Centos-6-x32, но как только epel выкатит пакеты для Centos-7 32-bit, будем обновляться. Все остальные манипуляции с серверам выполняются удаленно, при помощи ansible.

Структура проекта Ansible


В соответствии с принятой практикой, имеем такую файловую структуру:

$ find -type f
./roles/update_os/tasks/main.yml
./roles/update_nginx_configs/tasks/main.yml
./roles/update_nginx_configs/files/proxy.conf
./roles/update_nginx_configs/templates/domain.conf.j2
./roles/update_nginx_configs/handlers/main.yml
./roles/update_hostname/tasks/main.yml
./ansible.cfg
./hosts
./proxy-nodes.yml
Пройдемся по всем файлам.
./hosts
[test]
localhost ansible_connection=local

[centos:children]
proxy-nodes

[proxy-nodes]
xxx.xxx.xxx.xxx  ansible_connection=ssh ansible_ssh_user=root  ansible_ssh_pass=xxxxxx node_hostname=proxy-node-001.www.co
yyy.yyy.yyy.yyy  ansible_connection=ssh ansible_ssh_user=root  ansible_ssh_pass=yyyyyy node_hostname=proxy-node-010.www.co
zzz.zzz.zzz.zzz  ansible_connection=ssh ansible_ssh_user=root  ansible_ssh_pass=zzzzzz node_hostname=proxy-node-029.www.co

Тут отображена мета-группа [centos] и отдельно указана группа [proxy-nodes] как дочерня группа к [centos].
Структура с расчетом на расширение ролей и задач.

./ansible.cfg
[defaults]
pipelining                = True
hostfile                  = hosts
[ssh_connection]
ssh_args                  = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s
control_path              = ~/.ansible/cp/ansible-ssh-%%h-%%p-%%r


Тут тоже ничего особенного. Настройка сервера идет отпользователя root, поэтому можно смело включить pipelining.

hostfile — для умешения входных параметров при работе в консоли,
ssh_args — для уменьшения говорливости при релоадах хостов, и настройки persistent connection, среди них самый важный — ControlPath.

ControlPath - лучше один раз увидеть
$ ssh -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o ControlPath=/tmp/habr.socket root@192.168.124.185
Warning: Permanently added '192.168.124.185' (RSA) to the list of known hosts.
root@192.168.124.185's password:
Last login: Thu Mar 10 22:46:41 2016
[root@test001 ~]# service sshd stop
Stopping sshd: [ OK ]
[root@test001 ~]# exit
logout
Shared connection to 192.168.124.185 closed.
$ ssh -o ControlPath=/tmp/habr.socket root@192.168.124.185
Last login: Thu Mar 10 22:48:12 2016 from 192.168.124.1
[root@test001 ~]# exit
logout
Shared connection to 192.168.124.185 closed.
$ ssh root@192.168.124.185
ssh: connect to host 192.168.124.185 port 22: Connection refused
$ ssh -o ControlPath=/tmp/habr.socket root@192.168.124.185
Last login: Thu Mar 10 22:48:47 2016 from 192.168.124.1
[root@test001 ~]# service sshd start
Starting sshd: [ OK ]
[root@test001 ~]# exit
logout
Shared connection to 192.168.124.185 closed.
$ ssh root@192.168.124.185
Warning: Permanently added '192.168.124.185' (RSA) to the list of known hosts.
root@192.168.124.185's password:

Это позволяет работать быстрее, чем с опцией «accelerate: true». Несмотря на документацию, Centos 6 уже оооочень давно корректно работает с ControlPersist, и не требует такого преинсталла, как делалось ранее:

пример ./prepare-accelerate.yml для подготовки ноды к опции accelerate: true в плейбуке ./proxy-nodes.yml
---
- hosts: centos

  tasks:

  - name: install EPEL
    yum: name=epel-release

  - name: install keyczar
    yum: name=python-keyczar


Далее, стандартный плейбук, при работе с ролями, и таск update_os:

./proxy-nodes.yml
---
- hosts: proxy-nodes
  roles:
    - update_hostname
    - update_os
    - update_nginx_configs


./roles/update_os/tasks/main.yml
---

- name: repo install EPEL
  yum: name=epel-release

- name: repo install nginx-release-centos-6
  yum: state=present name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm

- name: packages install some
  yum: name={{ item }}
  with_items:
    - nginx
    - yum-update

- name: packages upgrade all
  yum: name=* state=latest


Считаю, что данные файлы не нуждаются в комментировании.

Роль update_hostname


Так уж повелось, что ноды как-то именуются. Как можно было видеть, в файле hosts указан, говорящий за себя, параметр node_hostname. К сожалению, ansible еще не может изменить хостнейм в соответствии с FQDN, поэтому приходится помогать:

./roles/update_hostname/tasks/main.yml
---

- name: set hostname
  hostname: name={{ node_hostname }}

- name: add hostname to /etc/hosts
  lineinfile: dest=/etc/hosts regexp='.*{{ node_hostname }}$' line="{{ ansible_default_ipv4.address }} {{ node_hostname }}" state=present create=yes
  when: ansible_default_ipv4.address is defined


Теперь hostname -f не ругается, а именно такая проверка существует в некоторых панелях управления.

Роль update_nginx_configs


Последняя роль — update_nginx_configs. Тут мы описываем обработчик, для релода nginx:

./roles/update_nginx_configs/handlers/main.yml
---
- name: reload nginx
  service: name=nginx state=reloaded


Следующий файл создает зону кеширования в разделе http, и инклюдит будущие домены для проксирования:

./roles/update_nginx_configs/files/proxy.conf
proxy_cache_path  /tmp  levels=1:2    keys_zone=PROXY:10m inactive=24h  max_size=4g use_temp_path=off;
include /etc/nginx/conf.d/proxy/*.conf;


Шаблон для доменов примерно такой:

./roles/update_nginx_configs/templates/domain.conf.j2
server {
        listen {{  ansible_default_ipv4.address  }}:80;
        server_name {{  item.domain  }} www.{{  item.domain  }};
        access_log  /var/log/nginx/{{  item.domain  }}.access.log main ;
        error_log   /var/log/nginx/{{  item.domain  }}.error.log;

        location / {
                proxy_pass http://{{  item.remoteip  }}:80/;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                client_max_body_size 10m;
                proxy_connect_timeout 90;

                proxy_cache             PROXY;
                proxy_cache_valid       200 302 1d;
                proxy_cache_valid       404    30m;
                proxy_cache_valid       any     1m;
                proxy_cache_use_stale   error timeout invalid_header updating http_500 http_502 http_503 http_504;
        }
}


Тут в целом ничего особенного, параметры проксирования и кеша подбираются под проект. Среди динамически параметров видим всего три: ansible_default_ipv4.address, item.domain и item.remoteip. Откуда берутся последние два, видно из следующего файла:

./roles/update_nginx_configs/handlers/main.yml
---

- name: create non existing dir /etc/nginx/conf.d/proxy/
  file: path=/etc/nginx/conf.d/proxy/ state=directory mode=0755

- copy: src=proxy.conf dest=/etc/nginx/conf.d/ owner=nginx group=nginx  backup=yes

- name: re-create domain templates
  template: src=domain.conf.j2 dest=/etc/nginx/conf.d/proxy/{{ item.domain }}.conf owner=nginx group=nginx  backup=yes
  with_items:
    - { domain: 'nginx.org'       , remoteip: '206.251.255.63' }
    - { domain: 'docs.ansible.com', remoteip: '104.25.170.30'  }
  notify: reload nginx

- name: validate nginx conf
  command: nginx -t
  changed_when: false


Вот и завершающие этапы: проверили, что директория для доменов существует, обновили конфиг с настройками зоны кеширования, в цикле с with_items прошлись по всем парам domain-remoteip и пересоздали конфиги.

Последним этапом идет валидация конфига, и при успешном результате запустится обработчик reload nginx. К сожалению, эту валидацию не получается использовать при генерации template или копировании конфига proxy.conf.

Опции validate=«nginx -t -c %s», или даже validate=«nginx -t -c /etc/nginx/nginx.conf -p %s» не так хороши, как в случае генерации конфига httpd.conf.

Поехали!


В случае обновления или изменения списка доменов в задаче «re-create domain templates», выполняем:
ansible-playbook proxy-nodes.yml

без каких-либо дополнительных параметров. После добавления новой ноды необходимо выполнить команду:
ansible-playbook proxy-nodes.yml --limit=bbb.bbb.bbb.bbb

где указать IP новой ноды.

Заключение


Спросив google, я не получил ответ о подобных сервисах от хостинг-провайдеров. А ведь целевая аудитория может быть очень разная, от CEO до различных adult web-мастеров.

Поэтому ниже опрос.
Как вы думаете, будет ли пользоваться такой сервис спросом?

Проголосовало 26 человек. Воздержалось 40 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (3)


  1. funny_sailor
    11.03.2016 10:21
    +5

    ansible_ssh_user=root ansible_ssh_pass=xxxxxx

    А почему не настроить авторизацию по ключу?


    1. BOPOHA
      11.03.2016 16:12

      С точки зрения скорости, за все время настройки или обновления конфигурации будет выполнена только одна авторизация на каждую ноду.

      Скьюрные вещи, это личное дело каждого.
      Кто понимает зачем надо ключи, молча, не задумываясь, сделает по ключам, повесит SSH на нестандартный порт, ограничит доступ фаерволом по IP… и т.д.
      Предлагая свой вариант, рискуешь нарваться на аналогичный вопрос "а почему без стука", и т.д.
      В общем оставим это за рамками статьи.


  1. nightvich
    11.03.2016 15:40
    +1

    На досуге почитайте PEP8.