Nginx. Установка и настройка. Часть 2 из 3

31.12.2023

Теги: CLIFastCGINginxКонфигурацияНастройкаСерверУстановка

Редиректы с кодом 301

Для сайта нужно настроить 301 редирект с домена www.example.com на домен example.com. Или наоборот, с домена example.com на домен www.example.com. Кроме того, нужно настроить редирект с http на https. И желательно — редирект с /some/path/index.php на /some/path/.

Здесь возможны различные конфигурации, вот два варианта

server {
    listen 80;
    server_name example.com www.example.com;
    # редирект с http на https
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # редирект с www.example.com на example.com
    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }

    # редирект с /some/path/index.php на /some/path/
    if ($request_uri ~ "^(.*)index\.(php|html)") {
        return 301 $1;
    }

    root /var/www/example.com/;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        # рекомендуемый разработчиками nginx файл конфигурации для работы с php-fpm
        include snippets/fastcgi-php.conf;
        # файл сокета для общения с php-fpm службой, которая будет выполнять php-код
        fastcgi_pass 127.0.0.1:9000;
    }

    error_page 404 403 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}
server {
    listen 80;
    server_name example.com www.example.com;
    # редирект с http на https
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name www.example.com;

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    # редирект с www.example.com на example.com
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    # редирект с /some/path/index.php на /some/path/
    if ($request_uri ~ "^(.*)index\.(php|html)") {
        return 301 $1;
    }

    root /var/www/example.com/;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        # рекомендуемый разработчиками nginx файл конфигурации для работы с php-fpm
        include snippets/fastcgi-php.conf;
        # файл сокета для общения с php-fpm службой, которая будет выполнять php-код
        fastcgi_pass 127.0.0.1:9000;
    }

    error_page 404 403 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

Как nginx обрабатывает запросы

Nginx ищет подходящий блок location по следующему алгоритму

  1. Поиск точного совпадения URI http-запроса со строкой location = string {...} без использования регулярных выражений. Если совпадение найдено — поиск завершается.
  2. Поиск совпадения начала URI http-запроса со строкой location [^~] string {...} без использования регулярных выражений. Идет поиск совпадения максимальной длины — это значит, что поиск не завершается после первого совпадения. Если совпадение максимальной длины имеет префикс ^~ — поиск завершается. Если совпадение максимальной длины не имеет префикса — location временно сохраняется.
  3. Поиск совпадения URI http-запроса с шаблоном location ~[*] regexp {...} с использованием регулярных выражений, в порядке их определения в файле конфигурации. Если совпадение найдено — поиск завершается. Обратите внимание, что поиск завершается после первого найденного совпадения. Звездочка * означает поиск без учета регистра.
  4. Если совпадения в третьем пункте не было — возвращается временно сохраненный location из второго пункта.

Если есть вложенные блоки location — алгоритм изменяется

  1. Поиск точного совпадения URI http-запроса со строкой location = string {...} без использования регулярных выражений. Если совпадение найдено — поиск завершается.
  2. Поиск совпадения начала URI http-запроса со строкой location [^~] string {...} без использования регулярных выражений. Идет поиск совпадения максимальной длины. После этого — переход внутрь и поиск точного совпадения или совпадения максимальной длины. При точном совпадении — поиск завершается. При совпадении максимальной длины — переход внутрь.
  3. Когда найдено совпадение максимальной длины на самом глубоком уровне — начинается поиск по регулярным выражениям. Как только будет найдено первое совпадение — поиск завершается. При этом, если не найдено совпадение на текущем уровне — происходит возврат на уровень выше и поиск там.
  4. После возврата на уровень выше, если текущий location не имеет префикса ^~ — поиск совпадения с регулярными выражениями на этом уровне. Если совпадение не найдено — переход на уровень выше. Если текущий location имеет префикс ^~ — сразу переход на уровень выше, без поиска на текущем уровне.

Тут важно понимать, что найденное совпадение максимальной длины — это только кандидат на возврат при работе алгоритма. Далее этот кандидат должен быть проверен на соответствие регулярным выражениям на текущем уровне и на всех уровнях выше. Если было совпадение с регулярным выражением — поиск завершается. Если совпадения не было — возвращается location максимальной длины (более подходящего кандидата не нашлось).

Примеры поиска подходящего location

Будем выполнять запрос /admin/login.php на разных конфигурациях и смотреть, какой location сработает.

Первая конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location /admin/ {
        return 200 "location A";
        # 2. совпадение с регулярным выражением
        location ~ \.php$ {
           return 200 "location D";
        }
    }

    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location D

На верхнем уровне было совпадение максимальной длины /admin/, внутри было совпадение с регулярным выражением \.php$.

Вторая конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location /admin/ {
        return 200 "location A";
        # 2. совпадение максимальной длины
        location /admin/login {
           return 200 "location B";
        }
    }

    # 3. совпадение с регулярным выражением
    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location C

На верхнем уровне было совпадение максимальной длины /admin/, внутри было совпадение максимальной длины /admin/login. Потом поиск по регулярным выражениям — но внутри /admin/ ничего нет. Поэтому подъем на уровень выше и поиск по регулярным выражениям на верхнем уровне — было найдено совпадение \.php$.

Третья конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location /admin/ {
        return 200 "location A";
        # 2. совпадение максимальной длины
        location /admin/login {
            return 200 "location B";
        }
        # 3. совпадение с регулярным выражением
        location ~ \.php$ {
           return 200 "location D";
        }
    }

    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location D

На верхнем уровне было совпадение максимальной длины /admin/, был переход внутрь этого location. Было совпадение максимальной длины /admin/login внутри /admin/. Потом поиск по регулярным выражениям внутри /admin/ — было найдено совпадение \.php$.

Четвертая конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location /admin/ {
        return 200 "location A";
        # 2. совпадение максимальной длины
        location /admin/login {
            return 200 "location B";
            # 3. совпадение с регулярным выражением
            location ~ \.php$ {
               return 200 "location D";
            }
        }
    }

    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location D

На верхнем уровне было совпадение максимальной длины /admin/, был переход внутрь этого location. Было совпадение максимальной длины /admin/login, был переход внутрь этого location. Потом поиск по регулярным выражениям внутри /admin/login — было найдено совпадение \.php$.

Пятая конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location /admin/ {
        return 200 "location A";
        # 2. совпадение максимальной длины
        location ^~ /admin/login {
            return 200 "location B";
        }
        location ~ \.php$ {
           return 200 "location D";
        }
    }

    # 3. совпадение с регулярным выражением
    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location C

На верхнем уровне было совпадение максимальной длины /admin/, был переход внутрь этого location. Было совпадение максимальной длины /admin/login внутри /admin/. Поиск по регулярным выражениям для этого совпадения максимальной длины запрещен — поэтому подъем на уровень выше. Потом поиск по регулярным выражениям на верхнем уровне — было найдено совпадение \.php$.

Шестая конфигурация

server {
    listen 80;
    server_name example.com www.example.com;

    default_type text/plain;

    # 1. совпадение максимальной длины
    location ^~ /admin/ {
        return 200 "location A";
        # 2. совпадение максимальной длины
        location ^~ /admin/login {
            return 200 "location B";
        }
        location ~ \.php$ {
           return 200 "location D";
        }
    }

    location ~ \.php$ {
        return 200 "location C";
    }
}
$ curl http://example.com/admin/login.php
location B

На верхнем уровне было совпадение максимальной длины /admin/, был переход внутрь этого location. Было совпадение максимальной длины /admin/login внутри /admin/. Поиск с использованием регулярных выражений для совпадения /admin/login запрещен — поэтому подъем на уровень выше. На верхнем уровне поиск с использованием регулярных выражений тоже запрещен. В итоге возвращается location максимальной длины /admin/login.

Обратите внимание, что запретить поиск с использованием регулярных выражений, используя префикс ^~ для location — можно только на уровне этого location. Поиск с использованием регулярных выражений внутри этого location будет выполняться всегда. Избежать этого можно только добавлением еще одного вложенного location с префиксом ^~.

Базовая HTTP-авторизация

Базовая авторизация HTTP — один из самых простых методов закрытия доступа к сайту, для этого предназначены две директивы

  • auth_basic позволяет либо отключить авторизацию с помощью значения off, либо задать текст, который выводится при входе
  • auth_basic_user_file определяет путь к файл специального формата с данными для входа, то есть имя пользователя и пароль

Путь к файлу с данными для входа может быть либо абсолютным либо относительным директории /etc/nginx. Содержимое файла должно быть в формате htpasswd для веб-сервера Apache2. Обычно для генерации такого файла используется утилита htpasswd, которая поставляется в пакете apache2-utils.

Давайте создадим файл /etc/nginx/htpasswd для доступа двух пользователей. Опция -c создает новый файл, так что использовать ее нужно только один раз. Обратите внимание, что повтороное использование опции удалит существующий файл паролей.

$ sudo htpasswd -c /etc/nginx/htpasswd evgeniy
New password: 123456
Re-type new password: 123456
Adding password for user evgeniy
$ sudo htpasswd /etc/nginx/htpasswd sergey
New password: qwerty
Re-type new password: qwerty
Adding password for user sergey
$ sudo cat /etc/nginx/htpasswd
evgeniy:$apr1$pvLcZg8s$H4W5clGwCf0WD05mx0wRp/
servey:$apr1$naGz3Lz5$OlTTOnU9QBCKyeBDeOtLp.

Кроме того, можно создать файл /etc/nginx/htpasswd с помощью утилиты openssl

$ sudo -i
[sudo] password for evgeniy: пароль
# echo "evgeniy:$(openssl passwd -apr1)" > /etc/nginx/htpasswd
Password: 123456
Verifying - Password: 123456
# echo "sergey:$(openssl passwd -apr1)" >> /etc/nginx/htpasswd
Password: qwerty
Verifying - Password: qwerty
# cat /etc/nginx/htpasswd
evgeniy:$apr1$Ib7QcYut$VmrV51vgHHxKl6DXMyERU0
sergey:$apr1$aWrII9Pa$l3qfdH/FA1K24Xng0BYcY.
# exit

Закроем раздел сайта для администратора

server {
    listen 80;
    server_name example.com www.example.com;
    # редирект с http на https
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # редирект с www.example.com на example.com
    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }

    # редирект с /some/path/index.php на /some/path/
    if ($request_uri ~ "^(.*)index\.(php|html)") {
        return 301 $1;
    }

    root /var/www/example.com/;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        # рекомендуемый разработчиками nginx файл конфигурации для работы с php-fpm
        include snippets/fastcgi-php.conf;
        # файл сокета для общения с php-fpm службой, которая будет выполнять php-код
        fastcgi_pass 127.0.0.1:9000;
    }

    location /admin {
        auth_basic "Restricted Content";
        auth_basic_user_file /etc/nginx/htpasswd;
        location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass 127.0.0.1:9000;
        }
    }

    error_page 404 403 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

Прочие директивы конфигурации

Директивы allow и deny

Часто возникает случай, когда требуется ограничить доступ к конкретному ресурсу на уровне ip-адресов. Для этого предназначены директивы allow и deny, которые принимают в качестве значения адрес или сеть, которым нужно разрешить или запретить доступ, либо служебное слово all, которое позволяет применить правило ко всем возможным хостам. По умолчаниию nginx разрешает доступ со всех адресов.

Рассмотрим пример конфигурации

server {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
}

Здесь разрешается доступ с адреса 127.0.0.1 (сам сервер) и из частной сети 10.0.0.0/8, а всем остальным доступ запрещается. Последовательность директив в nginx важна, поэтому, если первой идет директива deny all, то все остальные директивы allow не будут иметь эффекта.

Директивы allow и deny, казалось бы, можно использовать как firewall по ограничению доступа к ресурсам. Однако, в отличие от firewall, nginx при подключении все равно производит установку соединения, то есть открывает tcp-соединение, что требует ресурсов.

Директива client_max_body_size

Директива определяет, данные какого объема клиент может отправлять на сервер. Эта директива полезна при работе с приложениями, которые принимают файлы — к примеру, изображения или документы. Значением данной директивы выступает число в мегабайтах или килобайтах с приставками m или k. Либо число без приставки — в этом случае размер тела устанавливается в байтах. Значение ноль является специальным, которое отключает проверку на размер тела. Но лучше всегда использовать какой-то лимит, чтобы в nginx не отправляли гигантские файлы, с которыми он может не справиться.

Директивы limit_req_zone и limit_req

Директивы позволяют ограничить количество http-запросов от пользователей в определённый промежуток времени. Лимиты можно применять к простым GET-запросам домашней страницы сайта или же к POST-запросам формы авторизации.

http {
    limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/s;
    server {
        location /admin/ {
            limit_req zone=admin;
        }
    }
}

Директива limit_req_zone описывается в http секции конфигурации и может использоваться во множестве контекстов. Директива принимает три опции — key, zone и rate.

  • key — характеристика http-запросов для их группировки. В примере выше используется системная переменная $binary_remote_addr, которая содержит бинарные представления ip-адресов пользователей. Это означает, что лимиты из третьей опции будут применяться к каждому уникальному ip-адресу клиента из запроса. В примере используется переменная $binary_remote_addr, поскольку она занимает меньше места в памяти, чем её строковая альтернатива $remote_addr.
  • zone — зона разделяемой памяти, которая используется для хранения состояний ip-адресов и количества их обращений к разным URL-адресам. Эта память является общей для всех процессов nginx. Опция состоит из двух частей — названия зоны и объема памяти, разделенных двоеточием. Сведения о состоянии около 16000 ip-адресов занимают 1Мбайт, так что в нашей зоне можно хранить около 160000 записей.
  • rate — задаёт максимальное количество запросов. В примере выше будет принято 10 запросов в секунду. На самом деле, nginx измеряет количество запросов каждую миллисекунду, поэтому такой лимит означает 1 запрос каждые 100 миллисекунд. Поскольку мы не настраивали всплески (bursts), каждый следующий запрос, пришедший быстрее, чем через 100 мс после предыдущего, будет отброшен.
По поводу опции zone. Если места для добавления новой записи недостаточно, nginx удаляет самую старую запись, чтобы предотвратить исчерпание памяти. Если процесс nginx-а не может создать новую запись, из зоны может быть удалено до двух записей, которые не использовались в предыдущие 60 секунд. Если свободного пространства все равно не хватает, возвращается ошибка 503 (Service Temporarily Unavailable).

Директива limit_req_zone задаёт опции лимитов и общей памяти, но не управляет применением самих лимитов к запросам. Для окончательной настройки необходимо добавить в блоки location или server директиву limit_req. Выше мы применили лимиты для локации /admin/ в блоке server конфигурации. Тем самым мы установили лимит — 10 запросов в секунду с уникального ip-адреса.

Настройка Nginx как reverse proxy

У меня в локальной сети была одна виртуальная машина nginx-server (ip-адрес 192.168.110.50), которую мы настроили как веб-сервер. Создадим еще две виртуальные машины — www-one (ip-адрес 192.168.110.40) и www-two (ip-адрес 192.168.110.30). Виртуальная машина nginx-server теперь будет прокси-сервером, который отправляет http-запросы к example.com на виртуальные машины www-one и www-two.

1. Виртуальная машина nginx-server

Настройка Nginx как reverse proxy

Нам больше не нужна директория для хранения файлов сайта /var/www/example.com, так что нужно удалить либо переименовать.

$ sudo mv /var/www/example.com /var/www/example.com.old

Редактируем файл конфигурации, чтобы отправлять http-запросы к example.com на веб-сервер www-one.

$ sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;
    # редирект с http на https
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # редирект с www.example.com на example.com
    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }

    # редирект с /some/path/index.php на /some/path/
    if ($request_uri ~ "^(.*)index\.(php|html)") {
        return 301 $1;
    }

    # Следующие два заголовка передают на веб-сервер www-one настоящие схему и протокол, которые использовал
    # клиент при запросе. Поскольку мы на прокси-сервере всегда перенаправляем клиента на https, то значение
    # этих заголовков всегда будет https. Если же на прокси-сервере не выполянять редирект на https, то это
    # должен сделать www-one — и для этого ему нужны эти два заголовка, чтобы определить, нужно ли выполнять
    # перенаправление или нет.
    proxy_set_header X-Scheme https;
    proxy_set_header X-Forwarded-Proto https;
    # Передаем веб-серверу www-one заголовок Host из http-запроса клиента, то есть example.com. Прокси сервер
    # имеет две A-записи ДНС для example.com и www.example.com, которые указывают на него (это не так, потому
    # что мы только тестируем nginx, ip-адрес у прокси-сервера серый, ДНС-записей нет, есть только файл hosts).
    proxy_set_header Host $host;
    # К заголовку X-Forwarded-For, полученному от клиента, будет через запятую добавлено значение $remote_addr.
    # Если запрос от клиента не проходил через другие прокси, то значение X-Forwarded-For будет $remote_addr.
    # На веб-сервере www-one можно будет восстановить настоящий ip-адрес клиента — используя протокол PROXY
    # или модуль Nginx RealIP.
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # Передаем веб-серверу www-one настоящий ip-адрес клиента. На веб-сервере www-one нужно будет восстановить
    # настоящее значение адреса — это можно сделать, используя протокол PROXY или модуль Nginx RealIP.
    proxy_set_header X-Real-IP $remote_addr;
    # Директиву выше можно удалить, для восстановления настоящего ip-адреса клиента достаточно X-Forwarded-For.

    location / {
        proxy_pass http://192.168.110.40;
    }

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

В директории /etc/nginx есть файл proxy_params, который можно просто включить директивой include — чтобы не забыть, какие директивы нужны при настройке прокси-сервера.

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Когда nginx передает запрос, он автоматически вносит некоторые корректировки в заголовки запроса, получаемые от клиента

  • Nginx избавляется от пустых заголовков — нет смысла передавать пустые значения на другой сервер.
  • Nginx по умолчанию считает недействительным любой заголовок, содержащий символы подчеркивания — и удалит их из прокси-запроса. Это можно изменить с помощью директивы underscores_in_headers, если установить значение on.
  • Заголовок Host перезаписывается на значение, определенное в переменной $proxy_host. Значение этой переменной nginx берет из директивы proxy_pass. Это будет ip-адрес или имя и номер порта проксируемого сервера — нам это не подходит. Мы хотим, чтобы веб-сервер www-one считал, что он и есть example.com.
  • Заголовок Connection изменяется на close. Это говорит проксируемому серверу, что это соединение будет закрыто после ответа на запрос. Проксируемый сервер не должен ожидать, что это соединение будет постоянным.
Вообще, если nginx настроен как прокси-сервер, большинство настроек /etc/nginx/nginx.conf не имеют смысла. Потому как предназначены для веб-сервера, а требуется только перенаправлять http-запросы на www-one (и выполнять редиректы с http на https). Но отделить настройки, которые нужны (например ssl_) от настроек, которые не нужны (например gzip) — для меня слишком сложно. Остается надеяться, что nginx лишние настройки не будет использовать — без моего вмешательства.

Директива конфигурации proxy_pass

Когда для директивы proxy_pass не указывается URI — запрошенный клиентом URI будет передан на проксируемый сервер без изменений. Например, когда блок location в конфигурации ниже обрабатывает запрос /match/here/please — на upstream-сервер 192.168.110.40 будет отправлен запрос /match/here/please.

server {
    location /match/here {
        proxy_pass http://192.168.110.40;
    }
}

Когда для директивы proxy_pass указывается URI — часть запроса, которая соответствует определению location, заменяется этим URI. Например, когда блок location в конфигурации ниже обрабатывает запрос /match/here/please — на upstream-сервер 192.168.110.40 будет отправлен запрос /other/pass/please.

server {
    location /match/here {
        proxy_pass http://192.168.110.40/other/pass;
    }
}

При использовании домена вместо ip-адреса для проксируемого сервера — нужно добавить директиву resolver, чтобы nginx мог преобразовать доменное имя в ip-адрес. Кроме того, нужно проверить значение заголовка Host — потому что проксируемый веб-сервер может обслуживать несколько доменов.

resolver 1.1.1.1;

upstream example-domain {
    server example.com;
}

server {
    location /example/ {
        proxy_pass http://example-domain/;
        proxy_set_header Host example.com;
    }
}

По умолчанию nginx сам добавляет заголовок Host, устанавливая его в значение переменной $proxy_host. Значение переменной $proxy_host nginx получает из директивы proxy_pass. Если при этом используется директива upstream (группа серверов), то переменная $proxy_host будет пустая — а пустые заголовки nginx удаляет. В этом случае проскируемый веб-сервер не будет знать, какой домен должен отвечать — и отвечать будет сервер по умолчанию (см. часть 1).

2. Виртуальная машина www-one

Настройка nginx как веб-сервера

Нужно установить nginx и настроить его как веб-сервер

$ sudo apt install nginx

Редактируем основной файл конфигурации

$ sudo nano /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    # multi_accept on;
}

http {
    # базовые настройки
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    keepalive_timeout 40;
    
    # настройки кэширования
    open_file_cache max=10000 inactive=30s;

    # настройка mime-types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # настройки SSL
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    # логирование
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # разрешить сжатие
    gzip on;

    # виртуальные хосты
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Создаем файл конфигурации виртуального хоста

$ sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    # Можно доверять запросам, которые пришли с ip-адреса 192.168.110.50 и восстановить
    # настоящий ip-адрес клиента из заголовка X-Forwarded-For (это Nginx модуль RealIP)
    set_real_ip_from 192.168.110.50;
    real_ip_header X-Forwarded-For;

    root /var/www/example.com/;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        # рекомендуемый разработчиками nginx файл конфигурации для работы с php-fpm
        include snippets/fastcgi-php.conf;
        # файл сокета для общения с php-fpm службой, которая будет выполнять php-код
        fastcgi_pass unix:/run/php/php8.1-fpm-example-com.sock;
    }

    error_page 404 403 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

Создаем директорию для файлов виртуального хоста

$ sudo mkdir /var/www/example.com

Создаем файл php-скрипта для проверки

$ sudo nano /var/www/example.com/index.php
<h1>Веб-сервер на виртуальной машине www-one</h1>
<h3>Заголовки запроса</h3>
<pre>
<?php print_r(getallheaders()); ?>
</pre>
<h3>Массив $_SERVER</h3>
<pre>
<?php print_r($_SERVER); ?>
</pre>
<h3>Функция phpinfo()</h3>
<?php phpinfo(); ?>

Активируем этот виртуальный хост, создаем ссылку

$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

Установка и настройка php-fpm

Устанавливаем службу для выполнения php-скриптов

$ sudo apt install php8.1-fpm

Создаем пул php-fpm процессов для выполнения php-кода

$ sudo cp /etc/php/8.1/fpm/pool.d/www.conf /etc/php/8.1/fpm/pool.d/example-com.conf
$ sudo nano /etc/php/8.1/fpm/pool.d/example-com.conf
; имя пула, должно быть обязательно задано и быть уникальным
[example-com]

; пользователь и группа, от имени которого работают процессы
user = www-data
group = www-data

listen = /run/php/php8.1-fpm-example-com.sock

; пользователь и группа, которые могут читать и записывать unix-сокет
; здесь указываются пользователь и группа, под которым работает nginx
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Файл пула www.conf нужно удалить или переименовать

$ sudo mv /etc/php/8.1/fpm/pool.d/www.conf /etc/php/8.1/fpm/pool.d/www.back

Все готово, есталось перезапустить php-fpm.service и nginx.service

$ sudo systemctl restart php8.1-fpm.service
$ sudo systemctl restart nginx.service
Можно настроить реверс-прокси, чтобы принимать внешние https-соединения для разных доменов и направлять их на серверы внутри локальной сети. Главная проблема при https запросах в том, что данные зашифрованы и нельзя получить имя домена, которому предназначен запрос. Это решается с помощью расширения Server Name Indication (SNI) протокола Transport Layer Security (TLS). См. пример такой настройки здесь.

Если возникла проблема ссылок

Если сайт работает на какой-нибудь CMS, могут формироваться неправильные ссылки. Потому как веб-сервер www-one работает по протоколу http, а сайт example.com работает по протоколу https. Код php-скриптов может использовать значения из суперглобального массива $_SERVER, чтобы определить, как формировать ссылки на страницы сайта. Это могут быть ключи массива SERVER_PORT или REQUEST_SCHEME — сейчас они имеют значение 80 и http.

$ sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    # Можно доверять запросам, которые пришли с ip-адреса 192.168.110.50 и восстановить
    # настоящий ip-адрес клиента из заголовка X-Forwarded-For (это Nginx модуль RealIP)
    set_real_ip_from 192.168.110.50;
    real_ip_header X-Forwarded-For;

    root /var/www/example.com/;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        # рекомендуемый разработчиками nginx файл конфигурации для работы с php-fpm
        include snippets/fastcgi-php.conf;
        # файл сокета для общения с php-fpm службой, которая будет выполнять php-код
        fastcgi_pass unix:/run/php/php8.1-fpm-example-com.sock;
        # перезаписываем значения переменных SERVER_PORT/REQUEST_SCHEME на 443/https
        fastcgi_param SERVER_PORT 443;
        fastcgi_param REQUEST_SCHEME https;
    }

    error_page 404 403 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

Заголовки запроса

Array
(
    [Accept-Language] => ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
    [Accept-Encoding] => gzip, deflate, br
    [Sec-Ch-Ua-Platform] => "Windows"
    [Sec-Ch-Ua-Mobile] => ?0
    [Sec-Ch-Ua] => "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
    [Sec-Fetch-Dest] => document
    [Sec-Fetch-User] => ?1
    [Sec-Fetch-Mode] => navigate
    [Sec-Fetch-Site] => none
    [Accept] => text/html,application/xhtml+xml,application/xml;q=0.9,image/avif...
    [User-Agent] => Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
    [Upgrade-Insecure-Requests] => 1
    [Cache-Control] => no-cache
    [Pragma] => no-cache
    [Connection] => close
    [X-Real-Ip] => 192.168.110.2
    [X-Forwarded-For] => 192.168.110.2
    [Host] => example.com
    [X-Forwarded-Proto] => https
    [X-Scheme] => https
    [Content-Length] => 
    [Content-Type] => 
)

Массив $_SERVER

Array
(
    [USER] => www-data
    [HOME] => /var/www
    [HTTP_ACCEPT_LANGUAGE] => ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
    [HTTP_ACCEPT_ENCODING] => gzip, deflate, br
    [HTTP_SEC_CH_UA_PLATFORM] => "Windows"
    [HTTP_SEC_CH_UA_MOBILE] => ?0
    [HTTP_SEC_CH_UA] => "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
    [HTTP_SEC_FETCH_DEST] => document
    [HTTP_SEC_FETCH_USER] => ?1
    [HTTP_SEC_FETCH_MODE] => navigate
    [HTTP_SEC_FETCH_SITE] => none
    [HTTP_ACCEPT] => text/html,application/xhtml+xml,application/xml;q=0.9,image/avif...
    [HTTP_USER_AGENT] => Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
    [HTTP_UPGRADE_INSECURE_REQUESTS] => 1
    [HTTP_CACHE_CONTROL] => no-cache
    [HTTP_PRAGMA] => no-cache
    [HTTP_CONNECTION] => close
    [HTTP_X_REAL_IP] => 192.168.110.2
    [HTTP_X_FORWARDED_FOR] => 192.168.110.2
    [HTTP_HOST] => example.com
    [HTTP_X_FORWARDED_PROTO] => https
    [HTTP_X_SCHEME] => https
    [REDIRECT_STATUS] => 200
    [SERVER_NAME] => example.com
    [SERVER_PORT] => 443
    [SERVER_ADDR] => 192.168.110.40
    [REMOTE_USER] => 
    [REMOTE_PORT] => 
    [REMOTE_ADDR] => 192.168.110.2
    [SERVER_SOFTWARE] => nginx/1.18.0
    [GATEWAY_INTERFACE] => CGI/1.1
    [REQUEST_SCHEME] => https
    [SERVER_PROTOCOL] => HTTP/1.0
    [DOCUMENT_ROOT] => /var/www/example.com
    [DOCUMENT_URI] => /index.php
    [REQUEST_URI] => /
    [SCRIPT_NAME] => /index.php
    [CONTENT_LENGTH] => 
    [CONTENT_TYPE] => 
    [REQUEST_METHOD] => GET
    [QUERY_STRING] => 
    [SCRIPT_FILENAME] => /var/www/example.com/index.php
    [FCGI_ROLE] => RESPONDER
    [PHP_SELF] => /index.php
    [REQUEST_TIME_FLOAT] => 1704624200.685
    [REQUEST_TIME] => 1704624200
)
При проксировании запросов через proxy_pass на https-страницы у nginx по умолчанию есть особенность — он не проверяет SSL сертификат хоста, к кому обращается. В некоторых случаях (к примеру, в частных сетях, где используются самоподписанные сертификаты) это бывает очень полезно. Но об этом надо помнить при проверке доступности хоста через curl, который будет выдавать ошибку.

Балансировка нагрузки, директива upstream

Давайте на виртуальной машине www-two установим nginx и настроим его как веб-сервер. Все будет точь-в-точь как на виртуальной машине www-two — так что не буду повторять здесь. Тогда мы можем на прокси-сервере отправлять входящие http-запросы на эти веб-серверы по очереди. И тем самым делить нагрузку пополам. Конечно, нам нужно будет дополнительно позаботиться о том, чтобы эти два веб-сервера работали с одной файловой системой. Например, файлы сайта будут на еще одной виртуальной машине, а www-one и www-two будут монтировать себе директорию сайта по NFS.

Редактируем файл конфигурации на прокси-сервере

$ sudo nano /etc/nginx/sites-available/example.com
upstream backend {
    server 192.168.110.30;
    server 192.168.110.40;
}

server {
    listen 80;
    server_name example.com www.example.com;
    # редирект с http на https
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # редирект с www.example.com на example.com
    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }

    # редирект с /some/path/index.php на /some/path/
    if ($request_uri ~ "^(.*)index\.(php|html)") {
        return 301 $1;
    }

    location / {
        proxy_pass http://backend;
        include proxy_params;
    }

    ssl_certificate /etc/ssl/certs/example-com.crt;
    ssl_certificate_key /etc/ssl/private/example-com.key;

    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log error;
}

Round Robin — это метод балансировки нагрузки, когда у каждого сервера одинаковая возможность обрабатывать запросы. Кроме того, можно указать весовой коэффициент, который позволит повысить нагрузку одному и понизить нагрузку другому серверу. Сервер с наибольшим коэффициентом веса будет иметь приоритет (больше трафика) по сравнению с сервером с наименьшим коэффициентом.

upstream backend {
    server 192.168.110.30 weight=1;
    server 192.168.110.40 weight=2;
}

Метод наименьшего числа соединений (Least Connection) — еще один способ балансировки нагрузки. Работает путём отправки каждого нового запроса на сервер с наименьшим количеством активных соединений. Это гарантирует, что все серверы используются одинаково и ни один из них не перегружен.

upstream backend {
    least_conn;
    server 192.168.110.20;
    server 192.168.110.30;
    server 192.168.110.40;
}

Nginx анализирует http-запросы по мере их выполнения и пытается восстановить неудачные соединения для пассивной проверки работоспособности. Он идентифицирует сервер как недоступный и временно прекращает передачу запросов к нему, пока тот снова не будет признан активным. Управлять этим можно с помощью опций max_fails — кол-во неудачных попыток и fail_timeout — время, в течение которого должны произойти неудачные попытки.

upstream backend {
    server 192.168.110.20 max_fails=3 fail_timeout=60s;
    server 192.168.110.30;
    server 192.168.110.40;
}

Прокси-сервер для TCP/IP, директива stream

Давайте создадим еще две виртуальные машины sql-one (ip-адрес 192.168.110.60) и sql-two (ip-адрес 192.168.110.70) в нашей локальной сети. Уставновим и настроим на них MySQL кластер Galera типа master-master. Вообще говоря, желательно настроить кластер из трех серверов, чтобы при возникновении проблем на одном из них — два оставшихся могли создать кворум (т.е. большинство) и продолжить выполнение транзакций.

1. Установка на виртуальную машину sql-one

Для установки MySQL кластера Galera необходимо добавить репозиторий

$ curl -fsSL https://releases.galeracluster.com/GPG-KEY-galeracluster.com | sudo gpg --dearmor -o /usr/share/keyrings/galera-4.gpg
$ curl -fsSL https://releases.galeracluster.com/GPG-KEY-galeracluster.com | sudo gpg --dearmor -o /usr/share/keyrings/mysql-wsrep-8.gpg
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/galera-4.gpg] https://releases.galeracluster.com/galera-4/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/galera-4.list > /dev/null
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mysql-wsrep-8.gpg] https://releases.galeracluster.com/mysql-wsrep-8.0/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/mysql-wsrep-8.list > /dev/null
$ sudo nano /etc/apt/preferences.d/galera.pref
# Prefer Codership repository
Package: *
Pin: origin releases.galeracluster.com
Pin-Priority: 1001
$ sudo apt update
$ sudo apt install galera-4 mysql-wsrep-8.0

Во время установки нужно будет ввести пароль для пользователя root и выбрать аутентификацию с использованием пароля вместо плагина auth_socket.

Запускаем скрипт безопасности mysql_secure_installation. После ввода пароля root нужно ответить на несколько вопросов.

$ sudo mysql_secure_installation
Would you like to setup VALIDATE PASSWORD component? (Press y|Y for Yes, any other key for No) : N
Change the password for root ? (Press y|Y for Yes, any other key for No) : N
Remove anonymous users? (Press y|Y for Yes, any other key for No) : Y
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : Y
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : Y
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : Y

Включить или отключить компонент проверки сложности пароля можно с помощью запросов к базе данных

INSTALL COMPONENT 'file://component_validate_password';
UNINSTALL COMPONENT 'file://component_validate_password';

Мы отказались от проверки сложности пароля (нежелательно) и запретили удаленное подключение пользователя root к серверу. Удалили лишних пользователей базы данных и тестовую базу данных — нам они не нужны. Создадим пользователя для работы с базой данных и назначим ему права.

$ mysql -uroot -pqwerty -h127.0.0.1
> CREATE USER 'evgeniy'@'127.0.0.1' IDENTIFIED BY 'qwerty';
> GRANT ALL PRIVILEGES ON *.* TO 'evgeniy'@'127.0.0.1' WITH GRANT OPTION;
> CREATE USER 'evgeniy'@'192.168.110.50' IDENTIFIED BY 'qwerty';
> GRANT ALL PRIVILEGES ON *.* TO 'evgeniy'@'192.168.110.50' WITH GRANT OPTION;
> FLUSH PRIVILEGES;
> SELECT user, plugin, host, grant_priv FROM mysql.user;
+------------------+-----------------------+----------------+------------+
| user             | plugin                | host           | grant_priv |
+------------------+-----------------------+----------------+------------+
| evgeniy          | mysql_native_password | 127.0.0.1      | Y          |
| evgeniy          | mysql_native_password | 192.168.110.50 | Y          |
| mysql.infoschema | caching_sha2_password | localhost      | N          |
| mysql.session    | caching_sha2_password | localhost      | N          |
| mysql.sys        | caching_sha2_password | localhost      | N          |
| root             | mysql_native_password | localhost      | Y          |
+------------------+-----------------------+----------------+------------+
> exit

Создаем файл конфигурации

$ sudo nano /etc/mysql/mysql.conf.d/galera.cnf
[mysqld]
binlog_format=ROW
default-storage-engine=innodb
innodb_autoinc_lock_mode=2
bind-address=127.0.0.1,192.168.110.60
# Galera Provider Configuration
wsrep_on=ON
wsrep_provider=/usr/lib/galera/libgalera_smm.so
# Galera Cluster Configuration
wsrep_cluster_name="GaleraClaster"
wsrep_cluster_address="gcomm://192.168.110.60,192.168.110.70"
# Galera Synchronization Configuration
wsrep_sst_method=rsync
# Galera Node Configuration
wsrep_node_address="192.168.110.60"
wsrep_node_name="ClusterNodeOne"

Остановим службу mysql.service и запретим запуск при загрузке системы

$ sudo systemctl stop mysql.service
$ sudo systemctl disable mysql.service

2. Установка на виртуальную машину sql-two

Здесь все аналогично установке и настройке на виртуальной машине sql-one, так что не буду повторять.

$ sudo nano /etc/mysql/conf.d/galera.cnf
[mysqld]
binlog_format=ROW
default-storage-engine=innodb
innodb_autoinc_lock_mode=2
bind-address=127.0.0.1,192.168.110.70
# Galera Provider Configuration
wsrep_on=ON
wsrep_provider=/usr/lib/galera/libgalera_smm.so
# Galera Cluster Configuration
wsrep_cluster_name="GaleraClaster"
wsrep_cluster_address="gcomm://192.168.110.60,192.168.110.70"
# Galera Synchronization Configuration
wsrep_sst_method=rsync
# Galera Node Configuration
wsrep_node_address="192.168.110.70"
wsrep_node_name="ClusterNodeTwo"

На втором узле пользователя базы данных evgeniy создавать не нужно — при запуске кластера все таблицы синхронизируются и пользователь появится без нашего участия. Остановим службу mysql.service и запретим запуск при загрузке системы.

$ sudo systemctl stop mysql.service
$ sudo systemctl disable mysql.service

3. Запуск MySQL кластера Galera вручную

На виртуальной машине sql-one запускаем первый узел кластера

$ sudo /usr/bin/mysqld_bootstrap

На виртуальной машине sql-two запускаем второй узел кластера

$ sudo systemctl start mysql.service

Тут меня поджидал сюрприз — кластер упорно не хотел запускаться. Проверил лог ошибок /var/log/mysql/error.log на первом и втором узле — оказалось, обмен данными не происходит из-за ошибки.

2024-01-10T13:02:05.814106Z 0 [ERROR] [MY-000000] [WSREP] Failed to execute: wsrep_sst_rsync --role 'donor' --address...
2024-01-10T12:38:52.568122Z 0 [ERROR] [MY-000000] [WSREP] Failed to execute: wsrep_sst_rsync --role 'joiner' --address...

Исправить это легко — нужно на каждом узле выполнить команды

$ sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
$ sudo systemctl restart apparmor.service
Отключение AppArmor для MySQL следует выполнять с осторожностью, поскольку это может иметь последствия для безопасности. Отключение AppArmor для MySQL означает, что служба базы данных будет работать без ограничений на доступ к системным ресурсам.

4. Запуск MySQL кластера Galera скриптом

Если все узлы кластера были остановлены, то запускать кластер надо с последнего остановленного. Последний узел работал дольше остальных — и имеет самое продвинутое состояние. Кроме того, определить узел, с которого должен быть запущен кластер, можно с помощью файла /var/lib/mysql/grastate.dat. Давайте остановим службу mysql.service сначала на втором узле, потом на первом.

$ sudo systemctl stop mysql.service # на втором узле
$ sudo cat /var/lib/mysql/grastate.dat
GALERA saved state
version: 2.1
uuid:    29c071cf-afae-11ee-a958-13918117cc31
seqno:   100
safe_to_bootstrap: 0
$ sudo systemctl stop mysql.service # на первом узле
$ sudo cat /var/lib/mysql/grastate.dat
GALERA saved state
version: 2.1
uuid:    29c071cf-afae-11ee-a958-13918117cc31
seqno:   101
safe_to_bootstrap: 1

На первом узле значение safe_to_bootstrap имеет значение единица — это значит, что кластер нужно запускать, начиная с этого узла. Если мы перезагружаем все узелы кластера, на одном из них нужно выполнить /usr/bin/mysqld_bootstrap, а на всех остальных — systemctl start mysql.service. Поэтому мы отключили запуск службы mysql.service при загрузке системы на всех узлах.

Вообще, один узел всегда должен оставаться запущенным — в этом случае можно разрешить запуск службы mysql.service при загрузке системы на всех узлах кластера. Перезагрузка или остановка одного или двух узлов, когда остается запущенным хотя бы один узел — вполне безопасно, эти узлы после запуска службы mysql.service присоединятся к кластеру.

Давайте напишем скрипт, который будет выполняться при загруке на каждом узле и запускать кластер.

$ sudo -i
# nano /root/mysqld_bootstrap.sh
#!/bin/bash

# для отправки сообщения в телеграм
API_KEY='..........'
CHAT_ID='..........'
# отправлять сообщение в телеграм?
send_error=true

# этот узел кластера и другой узел кластера
this_node='192.168.110.60'
other_node='192.168.110.70'
mysql_port='3306'

# сколько секунд ждать запуска другого узла
try_count=60

function is_this_running {
    systemctl is-active mysql.service > /dev/null 2>&1 && echo 'yes' || echo 'no'
}

function is_other_running {
    (echo > /dev/tcp/$other_node/$mysql_port) > /dev/null 2>&1 && echo 'yes' || echo 'no'
}

function telegram {
    api_url="https://api.telegram.org/bot${API_KEY}/sendMessage"
    header='Content-Type: application/json; charset=utf-8'
    message=$1
    json='{\"chat_id\":\"%s\",\"text\":\"%s\"}'
    printf -v data "$json" "$CHAT_ID" "$message"
    curl -X POST -H "$header" -d "$data" $api_url > /dev/null 2>&1
}

function error_message {
    echo $1
    $send_error && telegram "$1"
    exit 1
}

function wait_other_starting {
    count=1
    while [ "$other_running" = 'no' ]; do
        echo "...Ждем запуска другого узла кластера $other_node..."
        sleep 1
        other_running=$(is_other_running)
        (( count++ ))
        if [ $count -gt $try_count ]; then
            error_message "Ошибка! Другой узел кластера $other_node не запустился"
        fi
    done
    echo "Другой узел кластера $other_node успешно запущен"
}

this_running=$(is_this_running)
if [ "$this_running" = 'yes' ]; then
    echo "Этот узел кластера $this_node уже запущен"
else
    echo "Этот узел кластера $this_node не запущен"
fi

other_running=$(is_other_running)
if [ "$other_running" = 'yes' ]; then
    echo "Другой узел кластера $other_node уже запущен"
else
    echo "Другой узел кластера $other_node не запущен"
fi

# если оба узла запущены, ничего не делаем
if [ "$this_running" = 'yes' -a "$other_running" = 'yes' ]; then
    echo 'Оба узла кластера запущены, нечего делать'
    exit 0
fi

# если этот узел запущен, ждем запуска второго
if [ "$this_running" = 'yes' -a "$other_running" = 'no' ]; then
    wait_other_starting
    exit 0
fi

# если другой узел запущен, запускаем этот узел
if [ "$this_running" = 'no' -a "$other_running" = 'yes' ]; then
    echo "Другой узел $other_node запущен, присоединяемся к кластеру"
    systemctl start mysql.service
    this_running=$(is_this_running)
    if [ "$this_running" == 'yes' ]; then
        echo "Этот узел кластера $this_node успешно запущен"
        exit 0
    else
        error_message "Ошибка! Не удалось запустить этот узел $this_node"
    fi
fi

# если оба узла не запущены, нужно выяснить, какой запускать первым
if [ "$this_running" = 'no' -a "$other_running" = 'no' ]; then
    if grep -q 'safe_to_bootstrap: 1' /var/lib/mysql/grastate.dat; then
        echo "Этот узел $this_node подходит для запуска кластера"
        /usr/bin/mysqld_bootstrap
        this_running=$(is_this_running)
        if [ "$this_running" = 'yes' ]; then
            echo "Этот узел $this_node запущен, ждем запуска другого"
            wait_other_starting
            exit 0
        else
            error_message "Ошибка! Не удалось запустить этот узел $this_node"
        fi
    else
        echo "Этот узел $this_node не подходит для запуска кластера"
        wait_other_starting
        systemctl start mysql.service
        this_running=$(is_this_running)
        if [ "$this_running" = 'yes' ]; then
            echo "Этот узел кластера $this_node успешно запущен"
            exit 0
        else
            error_message "Ошибка! Не удалось запустить этот узел $this_node"
        fi
    fi
fi
# chmod ug+x /root/mysqld_bootstrap.sh

На втором узле нужно изменить значения переменных this_node и other_node

# этот узел кластера и другой узел кластера
this_node='192.168.110.70'
other_node='192.168.110.60'

Теперь добавим этот скрипт в cron на обоих узлах кластера

# crontab -e
# время запуска — после каждой перезагрузки системы
@reboot /root/mysqld_bootstrap.sh

Если так случилось, что на всех узлах кластера safe_to_bootstrap: 0 — нужно на всех узлах запустить команду

$ sudo /usr/bin/wsrep_recover
WSREP: Recovered position 220dcdcb-1629-11e4-add3-aec059ad3734:1122
--wsrep_start_position=220dcdcb-1629-11e4-add3-aec059ad3734:1122

На каком узле число, выделенное красным, больше — с того узла и начинаем, то есть выполянем mysqld_bootstrap. Но перед этим нужно отредактировать файл /var/lib/mysql/grastate.dat на этом узле — изменить значение safe_to_bootstrap на единицу.

5. Настройка Nginx как TCP/IP proxy

Теперь настроим nginx на виртуальной машине nginx-server как tcp/ip прокси, чтобы принимать соединения на порту 33066 и отправлять их на виртуальные машины sql-one и sql-two на стандартный порт 3306. Давайте для начала создадим несколько директорий внутри /etc/nginx.

$ sudo mkdir -p /etc/nginx/proxy-http/available
$ sudo mkdir -p /etc/nginx/proxy-http/enabled
$ sudo mkdir -p /etc/nginx/proxy-stream/available
$ sudo mkdir -p /etc/nginx/proxy-stream/enabled

У нас все будет по аналогии с директориями sites-available и sites-enabled — в available будут файлы конфигурации, а в enabled — символические ссылки. И перенесем файл конфигурации sites-available/example.com — потому как раньше там была конфигурация сайта example.com, а теперь — конфигурация прокси-сервера (но название и размещение остались старые).

$ sudo mv /etc/nginx/sites-available/example.com /etc/nginx/proxy-http/available/example-com.conf
$ sudo rm /etc/nginx/sites-enabled/example.com
$ sudo ln -s /etc/nginx/proxy-http/available/example-com.conf /etc/nginx/proxy-http/enabled/

Отредактируем основной файл конфигурации

$ sudo nano /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    # multi_accept on;
}

http {
    # настройки SSL
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    # логирование
    access_log off;
    error_log off;

    # дополнительные файлы конфигурации
    include /etc/nginx/conf.d/*.conf;
    
    # настройки http-прокси
    include /etc/nginx/proxy-http/enabled/*.conf
}

stream {
    # настройки tcp/udp прокси
    include /etc/nginx/proxy-stream/enabled/*.conf;
}

Создаем файл конфигурации для проксирования запросов к MySQL кластеру

$ sudo nano /etc/nginx/proxy-stream/available/mysql-cluster.conf
upstream mysql-cluster {
    server 192.168.110.60:3306;
    server 192.168.110.70:3306;
}

server {
    listen 33066;
    proxy_pass mysql-cluster;
}

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

$ sudo ln -s /etc/nginx/proxy-stream/available/mysql-cluster.conf /etc/nginx/proxy-stream/enabled/

И перезагружаем nginx для применения новой конфигурации прокси-сервера

$ sudo systemctl restart nginx.service

Все готово, теперь можно с домашнего компьютера, на котором у меня запущены все пять виртуальных машин, выполнить соединение с кластером через прокси-сервер.

$ mysql -uevgeniy -pqwerty -h192.168.110.50 -P33066

Но при этом нельзя будет выполнить mysql-соединение с sql-one на sql-two и наоборот — потому что база данных принимает соединения только от пользователей evgeniy@127.0.0.1 и evgeniy@192.168.110.50. Впрочем, это легко исправить, если добавить пользователей evgeniy@192.168.110.60 и evgeniy@192.168.110.70.

6. Доступ по ssh через прокси-сервер

Хорошо бы еще добавить возможность ssh-подключений к серверам www-one, www-two, sql-one, sql-two через прокси-сервер nginx-server для администрирования.

$ sudo nano /etc/nginx/proxy-stream/available/ssh-access.conf
# виртуальная машина www-one
server {
    listen 2201;
    proxy_pass 192.168.110.40:22;
}
# виртуальная машина www-two
server {
    listen 2202;
    proxy_pass 192.168.110.30:22;
}
# виртуальная машина sql-one
server {
    listen 2203;
    proxy_pass 192.168.110.60:22;
}
# виртуальная машина sql-two
server {
    listen 2204;
    proxy_pass 192.168.110.70:22;
}

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

$ sudo ln -s /etc/nginx/proxy-stream/available/ssh-access.conf /etc/nginx/proxy-stream/enabled/

И перезагружаем nginx для применения новой конфигурации прокси-сервера

$ sudo systemctl restart nginx.service

Теперь можем подключаться к виртуальным машинам, указывая порт 2201, 2202, 2203, 2204.

$ ssh -p2201 evgeniy@192.168.110.50 # виртуальная машина www-one
$ ssh -p2202 evgeniy@192.168.110.50 # виртуальная машина www-two
$ ssh -p2203 evgeniy@192.168.110.50 # виртуальная машина sql-one
$ ssh -p2203 evgeniy@192.168.110.50 # виртуальная машина sql-two

Дополнительно

Поиск: CLI • Nginx • Конфигурация • Настройка • Установка • FastCGI • Сервер

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.