Примерно полгода назад, пришлось разработать схему обратного проксирования сайтов, с многих нод (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
Тут отображена мета-группа [centos] и отдельно указана группа [proxy-nodes] как дочерня группа к [centos].[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
Структура с расчетом на расширение ролей и задач.
./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:
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-мастеров.
Поэтому ниже опрос.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
funny_sailor
А почему не настроить авторизацию по ключу?
BOPOHA
С точки зрения скорости, за все время настройки или обновления конфигурации будет выполнена только одна авторизация на каждую ноду.
Скьюрные вещи, это личное дело каждого.
Кто понимает зачем надо ключи, молча, не задумываясь, сделает по ключам, повесит SSH на нестандартный порт, ограничит доступ фаерволом по IP… и т.д.
Предлагая свой вариант, рискуешь нарваться на аналогичный вопрос "а почему без стука", и т.д.
В общем оставим это за рамками статьи.