Привет всем,

У меня есть один проект, который представляет из себя сетап в два сервера с обычным LAMP-стеком. Оба сервера разнесены по разным хостинговым площадкам.

Ранее на этих серверах поднял site-to-site vpn, и настроил репликацию сервисов и базы данных. На текущем этапе встал вопрос, а как настроить файловер между серверами? - Что бы при проблемах на одном сервере, пользовательский трафик перекинулся на резервный.

Единственное решение, как сказал мой один коллега - дешево и сердито, это настроить фейловер на уровне dns.

Доменное имя хостится на Cloudflare, под бесплатным тарифным планом. На бесплатном аккаунте не доступны фишки балансировки, за включенние этого аддона надо доплатить 5$. Но мы можем использовать API Cloudflare для манипуляции над поддоменом, в обновлении его ip-адреса.

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

Получаем токен к API

Прежде чем начать, нужно получить токен для работы c API. Подключаемся к панели, и переходим в настройки своего профиля: cloudflare-update-1.png

Затем перемещаемся в раздел c api-токенами: cloudflare-update-2.png

На новом окне жмем на создание нового токена: cloudflare-update-3.png

Далее нужно будет выбрать шаблон с правами на использование api. Мы создадим свой кастомный токен. cloudflare-update-4.png

В менюшке создания токена, указываем имя для будущего токена. cloudflare-update-5.png А в разделе permissions даем доступ на взаимодействие с объекта DNS. Затем указываем, с какой именно зоной хотим работать.

Жмем подтверждение, и создаем новый токен: cloudflare-update-6.png

Откроется новая страничка с токеном, мы просто копируем его.

Пишем скриптец

В зоне моего домена я добавил запись с именем service.nixhub.ru с указанием ip-адреса первого сервера. Теперь напишем скриптец, который будет проверять доступность сервиса на первой ноде. cloudflare-update-9.png

И в случаи недоступности, отправит запрос в API Cloudflare на изменение ip-адреса. cloudflare-update-10.png

Нечего оригинальнее я не придумал, кроме того как осуществлять проверки доступности хоста через curl-запрос к вебсерверу и icmp-проверкой. Ну и собственно сам сприптец:

#!/bin/bash

CF_ZONE="nixhub.ru"
CF_NSRECORD="service.nixhub.ru"
CF_TOKEN="tokens"

IP_ADDR=$(curl -s -X GET https://checkip.amazonaws.com)
IP_DOMAIN=$(host $CF_NSRECORD | grep -Eioh "([0-9]{1,3}[\.]){3}[0-9]{1,3}")s

NGNX_AVABILITY=$(curl -s $CF_NSRECORD >>/dev/null && echo up || echo down)
HOST_AVABILITY=$(ping -c3 $CF_NSRECORD >>/dev/null && echo up || echo down)

LOG_PATH="/var/log/cfupdate.log"

function message_to_file() {
    MESSAGE=$1
    TIMESTAMP=$(date "+%Y-%m-%d/%H:%M")
    echo "$TIMESTAMP --> $MESSAGE" >> $LOG_PATH
}

function update_dns_record() {
    CF_ZONEID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE&status=active" \
      -H "Authorization: Bearer $CF_TOKEN" \
      -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

    CF_ARECORDID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records?type=A&name=$CF_NSRECORD" \
      -H "Authorization: Bearer $CF_TOKEN" \
      -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

    curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records/$CF_ARECORDID" \
        -H "Authorization: Bearer $CF_TOKEN" \
        -H "Content-Type: application/json" \
        --data "{\"type\":\"A\",\"name\":\"$CF_NSRECORD\",\"content\":\"$IP_ADDR\",\"ttl\":900,\"proxied\":false}" | jq
}


if [[ $IP_ADDR == $IP_DOMAIN ]]; then
    message_to_file "DNS Record don't needed update"
    exit
fi

if [[ $NGNX_AVABILITY = "down" || $HOST_AVABILITY = "down" ]]; then
    message_to_file "Nginx state is $NGNX_AVABILITY. Host is $HOST_AVABILITY. Perform cf failover."
    update_dns_record
fi

По классике, сначала определяем ряд статических переменных:

CF_ZONE="nixhub.ru"
CF_NSRECORD="service.nixhub.ru"
CF_TOKEN="token"
  • CF_ZONE - в значении этой переменной указывается доменная зона, с которой будут происходить изменения. И к которой мы ранее предоставили доступ по токену
  • CF_NSRECORD - тут указываем а-запись, которую в будущем и будем менять.
  • CF_TOKEN - здесь вставляем ранее полученный токен от cloudflare.
  • LOG_PATH - в этой переменной хранится путь к журналу (логу)

Далее список переменных, в значение которых подставляется результат выполнения команд:

IP_ADDR=$(curl -s -X GET https://checkip.amazonaws.com)
  • IP_ADDR - данная переменная будет помещать к себе результат выполнения команды curl, которая отправит запрос на внешний ресурс для получение текущего ip-адреса.

Последующие переменные, получают значения от проверок доступности первого сервера.

NGNX_AVABILITY=$(curl -s $CF_NSRECORD >>/dev/null && echo up || echo down)
HOST_AVABILITY=$(ping -c3 $CF_NSRECORD >>/dev/null && echo up || echo down)
  • NGNX_AVABILITY - тут в значении результат выполнения команды curl, которой мы дергаем страничку сайта и вывод перенаправляем в /dev/null. Затем через двойной амперсанд выполняется логической условие. Если сайт доступен, то значение будет строка up. Иначе помещается строка down.
  • HOST_AVABILITY - здесь также будет значения up или down, которые мы получем в результате выполнения команды ping.

В скриптец включил две функции.

function message_to_file() {
    MESSAGE=$1
    TIMESTAMP=$(date "+%Y-%m-%d/%H:%M")
    echo "$TIMESTAMP --> $MESSAGE" >> $LOG_PATH
}

function update_dns_record() {
    CF_ZONEID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE&status=active" \
      -H "Authorization: Bearer $CF_TOKEN" \
      -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

    CF_ARECORDID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records?type=A&name=$CF_NSRECORD" \
      -H "Authorization: Bearer $CF_TOKEN" \
      -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

    curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records/$CF_ARECORDID" \
        -H "Authorization: Bearer $CF_TOKEN" \
        -H "Content-Type: application/json" \
        --data "{\"type\":\"A\",\"name\":\"$CF_NSRECORD\",\"content\":\"$IP_ADDR\",\"ttl\":900,\"proxied\":false}" | jq
}
  • Функция message_to_file() в качестве одного аргумента принимает на себя какую-то строку. И далее к этой строке конкатинирует время, затем записывает все в журнал.

  • Функция update_dns_record() просто выполняет post-запрос на API Cloudflare, для изменения записи.

Во внутрь последней функции включены свои локальные переменные, которые хранят с себе значения id объектов cloudfrare. Значения данных переменных мы также получаем за счет отправки через curl запроса в API. Далее через пайп полученный вывод перенаправляем в jq и вычленяем нужные нам id или контент.

CF_ZONEID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE&status=active" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

CF_ARECORDID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records?type=A&name=$CF_NSRECORD" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

CF_CURRENT_IP=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records/$CF_ARECORDID" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .content')

Эти переменные вынес в функцию, что бы исключить лишние обращения в CloudFlare.

Ну и наконец, финальные условия:

if [[ $IP_ADDR == $CF_CURRENT_IP ]]; then
    message_to_file "DNS Record don't needed update"
    exit
fi

Этот условный оператор проверяет текущий ip-адрес ноды и ip-адрес закрепленный за А-записью в cloudflare. И если они совпадают, то скрипт просто завершает свою работу.

Следующее условие проверяет значения в переменных - NGNX_AVABILITY, HOST_AVABILITY. И если хоть одно значение down, то в сначала в лог добавится сообщение о переключении. Затем выполнится функция update_dns_record.

if [[ $NGNX_AVABILITY = "down" || $HOST_AVABILITY = "down" ]]; then
    message_to_file "Nginx state is $NGNX_AVABILITY. Host is $HOST_AVABILITY. Perform cf failover."
    update_dns_record
fi

Задание в cron

Теперь остается скриптец завернуть в cron-задание. Мне показалось логично создать задачу на втором сервере.

root@service2:~$ vim /etc/crontab
---
*/10 * 	* * *	root	/bin/bash /root/scripts/cfupdate.sh

В данном случаи сприпт будет запускаться каждые 10 минут.

Стоит отметить также момент, если в ваших dns включена опция proxy, то придется пересмотреть условия проверок доступности мастер сервера. Иначе нечего работать не будет.