Я работаю DevOps-инженером в команде разработки продукта Колибри-АРМ, аналога Microsoft SCCM, покрывающего потребности в импортозамещении ПО для управления парком АРМ. В данной статье будет описан кейс решения задачи по обеспечению высокой доступности продукта – она будет по большей части описывать перенос непосредственно функциональности, и тут не будут рассматриваться такие аспекты как безопасность кластера и приложения внутри.
С ростом количества клиентов, внедряющих наш продукт, начали появляться конкретные запросы: «Нам нужна отказоустойчивость или высокая доступность. И гарантия, что в случае сбоев ваш продукт будет работать». К решению задачи обеспечения высокой доступности мы с командой подошли со следующими вводными:
Наш продукт был только недавно переведен на контейнеры. До этого строился на основе systemd-сервисов.
Из-за решения проблем свежеиспеченной контейнеризованной версии состав сервисов часто менялся.
Вводная
Выбор инструментария
Исходя из эти данных был проведен небольшой сравнительный анализ. По большей части сравнивался входной порог в технологии оркестрации – процесса автоматизации развертывания, масштабирования и управления жизненным циклом контейнеров в кластерной системе, который позволяет эффективно управлять приложениями без ручного контроля.
Порог входа в технологию без переучивания и длительного погружения для того, чтобы выйти на нормальный уровень поддержки продукта, оказался Docker Swarm. На этом моменте меня и команду совершенно не смущало то, что в большом количестве мест говорят: "Docker Swarm хорошо подходит для маленьких проектов, а kubernetes существует в стольких реализациях - бери и пользуйся". Все эти предостережения были отклонены с комментарием: "У нас не настолько требовательный продукт, чтобы крутить его на HighLoad-инфраструктуре!".
Во время поиска информации я не нашел материалы, которые опишут практику, как перенести существующее приложение в кластер, обеспечивая его высокую доступность. Поэтому решил написать данную статью, взяв за основу наш кейс. P.S. Если вы натыкались на книги или статьи по этой теме – посоветуйте, буду благодарен!
Также при выборе инструментария необходимо было выбрать такое решение, которое выполнит задачу обеспечения высокой доступности и при этом не расширит сильно пул поддерживаемых нашей командой сервисов. Мы поняли: нам нужно было сделать решение таким, чтобы большая часть команды сопровождения могла без сильного переобучения продолжать поддержку решения.
Так как мы уже пошли по пути Docker Compose, после небольшого сравнения инструментов стало ясно, что логично было бы продолжить эту идею и использовать встроенный в Docker функционал по объединению виртуальных машин в Swarm-кластер. При этом мы не вводим принципиально другой инструментарий и можем обойтись простой передачей компетенций, связанных с углублением в Swarm.
Помимо непосредственно инструмента оркестрации еще были подняты вопросы по файловой системе, и единой точке входа в кластер. Подход остался примерно тот же – берем надежные инструменты, которые максимально просты в установке и настройке, а также просты для передачи компетенций по их поддержке.
Решение: kubernetes = overkill?
Итак, в ходе совещаний не раз возникала аналогия что использовать kubernetes как оркестратор для нашего продукта. Это как забивать гвозди телескопом – можно, но как будто бы что-то неправильно… А для поддержки kubernetes нужно изучить, как он работает со стороны администратора и со стороны пользователя. Научиться писать большое количество манифестов для внедрения уже новых сервисов в наш продукт. В этом плане может помочь kompose, однако он не даст экспертизы по kubernes – скорее, просто слегка облегчит вход. Как быть?
Три ноды – минимальный кластер из трех управляющих серверов
Для обеспечения отказоустойчивости и согласования кластера Docker Swarm использует алгоритм RAFT-консенсуса. Это работает из-за того что при внутренних голосованиях достигается кворум т.е. количество нод-менеджеров равное (N/2)+1, где N – это количество голосующих нод. Этот алгоритм позволяет иметь одну ноду-лидера в кластере, которая принимает решения.
При таком количестве из-за особенностей работы алгоритма-RAFT, в случае, когда одна из нод выходит из строя, оставшиеся члены кластера поддерживают рабочее состояние кластера, и планировщик перераспределяет нагрузку исходя из изменившегося состояния. В случае, если выбывшая нода была лидером кластера, оставшиеся ноды начинают раунд голосования для определения нового лидера.
Единая файловая система для приложения за счет GlusterFS
Теперь перейдем к файловой системе – стояла задача сделать 3 ноды, которые могут взаимозаменяться. Для этого было нужно, чтобы файлы приложения были синхронизированы между нодами.
Мы рассматривали разные варианты:
Один – с NFS-ресурсами, которые синхронизируются с помощью rsync-скрипта, и где один ресурс подключался ко всем нодам. Этот вариант был отвергнут из-за большого количества скриптов, которые нам нужно было сделать, чтобы обеспечить надежность метода.
Еще одним из вариантов было вынесение хранилища на отдельный сервер. В случае падения одной из нод это дало бы результат, и особенности реализации программного блочного хранилища тоже имели место быть, иными словами – программный iSCSI требует ручного управления блокировками, а это уже сильно усложняет реализацию. И тут вопрос сервера хранилища все еще остался не покрыт. Отдельный сервер сам по себе не отвечал требованиям к высокой доступности.
Отдельный аппаратный storage с общим LUN, подключенным ко всем нодам. Это идеальный сценарий, но, как и все идеальное – далекое от реальной жизни. Чаще всего, жизненный цикл использования Колибри-АРМ начинается с пилотного внедрения в некотором тестовом контуре, где подобного оборудования банально нет. Но если вы внедряете сразу в продуктивный контур – лучше так.
В итоге перебор вариантов сошелся к распределенным файловым системам. Сравнивали по большей части Ceph и GlusterFS. Lustre не рассматривали из-за сильной ориентированности на очень большие хранилища. Основным критерием выбора по большей части был тот же – низкий уровень входа для команды поддержки.
По итогу небольшого исследования уровень входа ниже оказался у GlusterFS, условно установил, запустил, создал том, примонтировал. Здесь не будет глубокой аналитики на тему плюсов и минусов GlusterFS и Ceph. Ниже приведена краткая таблица по сравнению.
Система |
Требования к ресурсам |
Требования к компетенциям |
Особенности |
Ceph |
Высокие (CPU, RAM, сеть) |
Высокая (знание CRUSH, pools, мониторинг) |
Объектное хранение, высокая отказоустойчивость и масштабируемость |
Lustre |
Очень высокие (специализированные серверы метаданных) |
Высокая (HPC навыки, сетевые протоколы) |
Высокая производительность, HPC-ориентирован |
GlusterFS |
Низкие/средние, без выделенных серверов метаданных |
Средние (базовые навыки распределённого хранения) |
Просто масштабируется, легко устанавливается, хуже с метаданными и производительностью при нагрузках |
К тому же, сразу из коробки GlusterFS предлагает возможность создать реплицируемый том, что подходит к нашей концепции заменяемых нод.
Стоит отметить, что для крупных и серьезных внедрений мы рекомендуем не использовать GlusterFS, и вместо этого отдавать предпочтение зрелым аппаратным решениям хранения данных. Это связано с тем, что GlusterFS является менее стабильной реализацией, которая может не соответствовать высоким требованиям к надежности и производительности в больших кластерах. Поэтому материал больше ориентирован на небольшие кластеры с умеренными требованиями, где простота установки и поддержки важнее максимально высокой отказоустойчивости.
Создание единой точки входа с помощью keepalived
После того как собрали кластер Docker Swarm и настроили файловое хранилище для контента контейнеров, необходимо как-то обращаться к приложению, развернутому в кластере. Сам по себе Docker Swarm имеет интересную особенность: каждая менеджер-нода кластера является точкой входа в кластер. Поэтому получить доступ к приложению можно, обратившись к любой ноде кластера. Мы выделили отдельный ip-адрес под виртуальный и настроили keepalived для выдачи этого виртуального ip-адреса одной из нод.
Ход работ
Стадия 0. Ничего не понятно, но очень интересно. Определение плана работ
Определившись с инструментами, начали прорабатывать то, как, собственно, перетащить наш по сути монолит, недавно разделенный на контейнеры в кластер для выполнения поставленной задачи. Прежде всего нужно настроить сам кластер, куда будем развертывать приложение. Для начала работы хватит GlusterFS и Docker Swarm.
После того как развернули основу, на которой будем размещать приложение, нужно попробовать развернуть наше приложение как мы это делаем обычно – с использованием нашего инсталлятора. Также стоит пока привязать все контейнеры к одной ноде. Это нужно для того, чтобы в дальнейшем по одному сервису отлаживать все проблемы, связанные с сетью и распределением контейнеров по кластеру. Следующим этапом мы начинаем убирать привязку к одной ноде по одному сервису и смотреть взаимодействие.
По завершению переноса всех сервисов важным продолжающим шагом будет описать что изменилось в конфигурации сервисов мононоды – для того, чтобы сформировать принцип, по которому можно будет переписывать уже новые сервисы под Swarm-развертывание. После этих шагов можно приступить к полноценному QA-тестированию.
А завершив тестирование и исправив все найденные проблемы, приступаем к завершающему эту итерацию шагу – написанию скриптов для облегчения развертывания кластера и скриптов для помощи существующему установщику и развертыванию в кластере продукта.
Стадия 1. GlusterFS – развертываем реплицируемую файловую систему
На самом деле, если почитать документацию и статьи по GlusterFS, можно понять, что процесс развертывания очень прост. Он сводится к нескольким шагам:
Добавление отдельного диска на каждую из трех нод.
Разметка диска, создание на нем файловой системы.
Монтирование раздела.
Установка GlusterFS.
Добавление нод в кластер GlusterFS.
Создание распределенного тома.
Монтирование распределенного тома.
Profit!
Схема томов выглядит следующим образом:

На схеме определено 2 тома, один под контент приложения, второй под хранилище образов.
Стадия 2. Переписываем композ файлы. Развертываем приложение в Docker Swarm, но привязываем контейнеры пока к одной ноде
Разобравшись с GlusterFS, пришло время запускать контейнеры в Docker Swarm, что для этого нужны docker-compose.yml файл и включенный режим swarm на ноде.
Для того, чтобы включить swarm режим на ноде достаточно выполнить команду:
sudo docker swarm init --advetise-addr <node_ip>
Выводом этой команды будет сообщение о том, что кластер инициализирован, и также будет предоставлена команда, выполнив которую на другом сервере, можно присоединиться к кластеру в роли worker-ноды.
Самое время немного рассказать о двух ролях нод в Swarm: ниже приведена таблица с разницей.
Роль |
Основные функции |
Управление кластером |
Запуск сервисов |
Особенности |
Manager |
Управляет состоянием кластера, принимает решения. |
Да, хранит данные RAFT consensus. Обрабатывает команды создания/обновления/удаления сервисов. |
Может запускать сервисы, но первоочередная нагрузка – управление кластером. |
Среди менеджеров выбирается лидер (Leader), остальные – запасные (Reachable) для обеспечения отказоустойчивости. Кластер может работать без Worker-нод, при этом Manager-ноды выполняют все задачи. |
Worker |
Выполняет задачи(распределение контейнеров, запуск сервисов). |
Нет |
Да |
Используются только для выполнения заданий, не управляют кластером. |
Инициализировав кластер, можно запустить любой compose-сервис следующей командой:
sudo docker stack deploy -c /path/to/docker-compose.yml stack_name
Эта команда считает docker-compose.yml построит конфиг для docker swarm и поместит конфигурацию в кластер, создаст необходимые сети, если они не созданы и запустит сервис. На стадии планирования казалось, что это quick win. Мы взяли сервисы и просто запустили в swarm. И получили вот это:
root@CARMSWARMMGRV01:/opt/colibri# docker stack deploy -c /opt/colibri/rabbitmq/docker-compose.yaml colibri
time="2024-12-19T11:05:40+03:00" level=warning msg="ignoring IP-address (127.0.0.1:15672:15672/tcp) service will listen on '0.0.0.0'"
Ignoring unsupported options: restart
Ignoring deprecated options:
container_name: Setting the container name is not supported.
Since --detach=false was not specified, tasks will be created in the background.
In a future release, --detach=false will become the default.
Creating network rabbitmq
failed to create network rabbitmq: Error response from daemon: network with name rabbitmq already exists
root@CARMSWARMMGRV01:/opt/colibri# docker stack deploy -c /opt/colibri/elevated-server/docker-compose.yml colibri
Ignoring unsupported options: build, devices, privileged, restart
Ignoring deprecated options:
container_name: Setting the container name is not supported.
Since --detach=false was not specified, tasks will be created in the background.
In a future release, --detach=false will become the default.
Creating service colibri_elevated-server
Как оказалось, есть один большой нюанс: Docker Compose и Docker Swarm – это два разных продукта. А то, что они используют одни и те же docker-compose.yml для запуска – скорее удобное обстоятельство.
На самом деле, Docker Swarm с определенного момента входит в состав пакета docker-cli, и синтаксис, который используется в описании сервисов, во многом пересекается с Compose. Однако Compose разрабатывался изначально под локальное развертывание на одной ноде. В отличии от него, Swarm разрабатывался как легковесный оркестратор – для того, чтобы объединить несколько Docker хостов в единый кластер.
Изначально наши сервисы писались под Compose, соответственно сейчас мы столкнулись с проблемой совместимости инструкций в описании файлах docker-compose.yml.
Выделил основные проблемы:
Решить, откуда брать образы контейнеров.
Описать список портов, а не выставить контейнер в режиме
network_mode: host.Описать тип контейнера (реплицируемый или глобальный).
Размещение контейнеров в кластере.
Препроцессинг конфигурации
docker-compose.yml.Переместить директории на хосте в одну директорию под GlusterFS для того, чтобы на всех нодах был одинаковый контент.
Порядок запуска сервисов.
Описать необходимые капабилити для контейнеров, которым необходим привилегированный режим.
1. Откуда брать образы контейнеров?
Перед тем, как проводить какие-либо изменения конфигураций, необходимо решить вопрос: откуда брать контейнеры нодам кластера. Загружать набор контейнеров по ftp, потом локально их грузить в docker – сразу показалось глупой идеей. Поэтому тут быстро было определено: нужно поднимать локальный реестр на основе registry.
Был написан скрипт, который генерирует сертификаты, docker-compose.yml и настраивает ноды на доверие этому реестру, затем поднимает сам реестр и загружает в него образы, которые использовались файлами Compose нашего продукта.
Перед тем, как выполнять скрипт, подняли отдельный распределенный том, чтобы реестр мог подняться на любой ноде и у него был актуальный список образов контейнеров.
А в директиве image в файлах конфигурации docker-compose.yml прописывался конкретный реестр а-ля:
---
services:
my_awesome_app:
image: registry.app.example.ru:5500/my_awesome_app:1.0
Выполнив эту подготовку, все ноды кластера могут получить нужные нам образы контейнеров в закрытом контуре без особых проблем.
2. Порты и сетевое взаимодействие
Начнем со списка портов. Переход продукта на кластерную версию был по внутреннему плану, который был составлен в результате небольшого ресерча на тему Best Practice. Я его тут публиковать не буду, но если описать его одной фразой, получится: «Бери альпину или минимальный дебиан и грузи в harbor», ну и много мелочей на подобие: как назвать директорию, в которой хранятся логи, контент, какое название Compose-файлов и т.д.
По содержимому docker-compose.yml каждый писал так, как думал, что правильно. Поэтому в большинстве случаев контейнеры мапились напрямую в сеть хоста, потому что так легче. Swarm не работает с таким режимом сети, т.к. для объединения хостов настраиваются оверлейные сети, внутри которых происходит взаимодействие между сервисами. И поэтому секция ports становится обязательной к заполнению.
Есть разные способы узнать порт, который слушает приложение в контейнере. Это и простое чтение документации контейнера, который ты используешь для своего продукта, если речь идет о third party решении (например, keycloak или superset). Если это контейнер, который делаете вы или ваша команда разработки, то можно узнать, какой порт задействует приложение – вплоть до того, чтобы запустить контейнер в песочнице и посмотреть, какие порты занимает процесс изнутри контейнера.
Следующим пунктом стало обращение сервисов друг к другу. Тут снова обращаю внимание на разницу между Swarm и Compose. В Compose мы можем просто назвать контейнер как хотим через секцию container_name: my_favorite_container, и если два контейнера добавлены в одну сеть, то из другого контейнера мы можем просто обратиться по этому имени:
ping my_favorite_container
В случае со Swarm добавляется такое понятие как сервис – основная единица, с которой мы работаем в Swarm, конфигурация, по которой создаются сущности Task, которые уже являются контейнерами. Сервис имеет свой ip-адрес, и при обращении к нему Swarm сам определяет, на какой контейнер отправлять трафик. Внутри сервиса может быть не одна реплика контейнера, также контейнеры внутри сервиса могут пересоздаваться, а ip-адрес при этом останется одним и тем же.
Перейдем к именованию. При создании контейнера на ноде Swarm задает имя по следующему шаблону:
<stack_name>_<service_name>.<replica_number>.<task_id>
Это для нас означает, что один и тот же контейнер будет иметь разные имена, и к нему не получится свободно обратиться тем же способом, что и раньше. Однако можно обратиться непосредственно к сервису, который тоже имеет шаблонное имя, но при этом при пересоздании оно меняться не будет. Шаблон имени сервиса сводится к <stack_name>_<service_name>
Откуда берутся stack_name и service_name?
-
stack_name – имя стека, которое задается при деплое последним агрументом
sudo docker stack deploy -c /path/to/docker-compose.yml perfect-stack
-
service_name – наименование внутри Compose-файла, в котором мы описываем ресурс, который намереваемся запустить:
--- services: nginx: # Это и есть service_name image: nginx:latest deploy: mode: global
Итого у нас при деплое создастся сервис с именем perfect-stack_nginx, и мы можем обращаться к контейнерам в этом сервисе по этому имени так же, как мы могли обращаться к my_favorite_container. Теперь осталось поменять межсервисное взаимодействие с ip-адресов и localhost на имена сервисов.
3. Типы контейнеров в кластере
Следующим моментом, с которым столкнулись – это функции, которые нужны для логики Compose. Для нас это были политики рестарта.
Дело в том, что Swarm – это уже оркестратор, и поэтому он имеет на своем борту функционал запуска контейнера, отслеживания его состояния, репликации и прочее полезное. Поэтому нужно было определить секцию deploy, которую мы заполняем теми параметрами, которые позволят нам добиться желаемого результата.
Например, в первую очередь нам было необходимо было решить, какие контейнеры у нас запускаются в одном экземпляре, а какие должны быть на каждой ноде. Почти все контейнеры попали в первую группу, за исключением vector, nginx и сервиса лицензирования.
Описание этих сервисов отличалось секцией deploy:
---
services:
nginx:
image: nginx:latest
deploy:
mode: global
---
services:
conjur_server:
image: bitnami/conjur:latest
deploy:
mode: replicated
replicas: 1
4. Размещение контейнеров в кластере
Помимо определения типа контейнеров в кластере, директива deploy имеет еще одну важную для нас сейчас возможность. А именно – задать ограничение на размещение контейнеров, привязывая их к конкретным нодам. Делается это следующим образом:
---
services:
conjur_server:
image: bitnami/conjur:latest
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.Hostname = carmasswrmv01
Для чего нам это нужно сейчас? мы запустить приложение внутри Swarm и разделить на разные этапы запуск контейнеров внутри оркестратора и отладку сетевого взаимодействия контейнеров, размещенных на разных нодах.
5. Переменные в docker-compose.yml
Двигаемся дальше. В Compose есть одна очень классная функция – препроцессинг конфигурации. Это означает, что если в docker-compose.yml файле используются переменные для конфигурирования самого docker-compose.yml (не путать с секцией environments), то, перед тем как запустить контейнеры, Compose сначала подтянет в конфигурацию эти переменные и подставит, а после уже начнет запускать контейнеры.
Пример:
---
services:
my_awesome_dotnet:
image: my_dotnet_app:l:1.0
ports:
- 8085:8085/tcp
command: gosu ${APP_USER:-dotnet} /usr/local/bin/docker-init.sh
При запуске через Compose он подставит вместо ${APP_USER:-dotnet} либо dotnet как значение по умолчанию либо содержимое переменной окружения APP_USER, если она установлена.
Без установленной переменной окружения в директиву command подставляется значение по умолчанию:
services:
my_awesome_dotnet:
image: my_dotnet_app:l:1.0
ports:
- 8085:8085/tcp
command: gosu dotnet /usr/local/bin/docker-init.sh
При установленной переменной окружения APP_USER равной test_usr в директиву command попадает уже значение не по умолчанию, а test_us:
services:
my_awesome_dotnet:
image: my_dotnet_app:l:1.0
ports:
- 8085:8085/tcp
command: gosu test_usr /usr/local/bin/docker-init.sh
При этом не нужно самому лезть в docker-compose.yml и править. Swarm этого функционала не имеет, он просто берет конфигурацию и сразу пытается ее применить в кластер. Можно использовать для решения этого вопроса следующий подход:
использовать
docker compose -f /path/to/docker-compose.yml config,с помощью этой команды Compose препроцессит конфигурацию,
так мы используем и функционал препроцессинга от Compose, и отказоустойчивость Swarm
win-win!
Однако для нашего решения был выбран вариант проще: все манипуляции с переменными окружения были перенесены на этап выполнения docker-init.sh-скрипта, который является entrypoint'ом для большинства наших контейнеров. Тем самым мы избавились от необходимости препроцессинга переменных в конфигурации.
6. GlusterFS и volumes
Дизайн нашего высокодоступного кластера предполагает, что на каждой ноде кластера монтируется распределенный сетевой том, файлы внутри которого имеют одинаковый контент. В текущей реализации это делается, чтобы не переделывать механизм установщика, который создает директорию, наполняет ее контентом и настраивает сервисы.
Конечно, можно было использовать плагины для docker, и с помощью GlusterFS “нарезать вольюмов” для контейнеров, чтобы к ним присоединять и отвязаться от хостовой файловой системы. Однако это потребовало бы глубокого рефакторинга механизма инсталляции продукта.
Вместе с этим обнаружилась небольшая особенность – инсталлятор не все файлы контейнеров кладет в одну директорию /opt/awesome_app. Некоторые сервисные файлы были в /srv, некоторые в /var/lib, а часть и вовсе в /var/log. Все, кроме /var/log, было перенесено в директорию /opt/awesome_app, а логи сервисов решили не смешивать.
7. Порядок запуска сервисов
Оркестратор имеет замечательную функцию: при падении контейнера он поднимает новый и убивает упавший. Это, с одной стороны, хорошо. С другой стороны, есть особенность конкретно Swarm. Если сервисы одного стека хранятся не в одном docker-compose.yml, то управлять запуском сервисов не получится. Директива depends-on смотрит на сервисы внутри одного конфигурационного файла.
Тут же сам функционал перезапуска контейнеров нам помогает. Если сервис очереди не запущен – контейнер, требующий очередь и который крашится, если сервис очереди не найден, будет перезапускаться до тех пор, пока сервис очереди не будет запущен. В такой зависимости контейнеров друг от друга, функционал управления порядка запуском нужен для того, чтобы избежать флуда в логах. На данный момент эти издержки принимаются, однако все равно хотелось бы иметь возможность задать порядок внутри стека.
8. Capabilities
Отдельно стоит пункт про capabilities, несмотря на его краткость. В Swarm нет директивы privileged: true , зато есть cap_add. Читаем про capabilities и выдаем контейнерам только необходимые. Про безопасность контейнеров хотел бы сделать отдельную статью.
Стадия 3. Разбираемся с сетью, начинаем распределять контейнеры по кластеру
К этой стадии мы подходим все еще не “высокодоступно”. Однако здесь можно сделать оговорку, что наконец запустили, и можно этот результат зафиксировать. На этой стадии нам предстоит простое дело: поправить в конфигурационных файлах адреса обращений между сервисами и поочередно снять ограничение на размещение контейнеров на одной ноде, убрав из конфигурации часть, описывающую это ограничение:
---
services:
conjur_server:
image: bitnami/conjur:latest
deploy:
mode: replicated
replicas: 1
###################
# Часть конфигурации, относящаяся к ограничению на размещение
#
placement:
constraints:
- node.Hostname = carmasswrmv01
#
###################
Убрав ограничение, мы передеплоим сервис и проверяем сетевое взаимодействие между сервисами удобными способами – к примеру, установкой telnet, curl и прочих утилит, помогающих провести диагностику.
Важно: в продуктивной версии контейнера такие диагностические утилиты должны отсутствовать для уменьшения вектора атаки.
Стадия 4. Сравнение итоговых конфигураций кластерного решения с решением мононоды
Немного повторюсь, что итоговые конфигурации отличаются по следующим пунктам:
наименование сервиса: не должно в себя включать наименование стека, так как оно будет идти префиксом;
image: должна содержать адрес registry;
ports: если к сервису приходят обращения из вне кластера, описываем конкретные порты для взаимодействия;
deploy: описываем тип контейнера, реплицируемый/глобальный. Задает политику перезапуска;
capabilities: для каждого контейнера необходимо определить только необходимые capability для обеспечения безопасности.
volumes: добавляем те директории, которые на хосте являются GlusterFS сетевым томом.
Стадия 5. Написание скриптов-обвязок для упрощения инициализации кластера на других площадках
Для процесса установки было написано несколько скриптов обвязок и инструкций, чтобы упростить процесс установки продукта в Swarm-режиме. Эти скрипты делятся на 2 категории: те, которые выполняются перед установкой продукта, и те, которые выполняются после установки продукта. Разберем сам процесс установки.
Подготовка хранилища
Схема будет выглядеть следующим образом. Для ее реализации необходимо выполнить следующие шаги:
-
на каждой ноде разметить диск /dev/sdb на 2 раздела, которые будут монтироваться в
/data/colibri_brick/data/colibri_registry_data_brick
-
на каждой ноде установить утилиты GlusterFS и сам glusterd
sudo apt install glusterfs-client glusterfs-common glusterfs-server –y
-
на любой из нод инициализировать кластер добавив все ноды
sudo gluster volume create colibri_volume replica 3 \ carmasswrmv01:/data/colibri_brick \ carmasswrmv02:/data/colibri_brick \ carmasswrmv03:/data/colibri_brick force sudo gluster volume start colibri_volume
-
на каждой ноде добавить сетевой том в /etc/fstab указав параметр x-systemd.automount
echo $(hostname -f)":/colibri_volume /opt/colibri glusterfs _netdev,acl,x-systemd.automount 0 0" | sudo tee -a /etc/fstab
перезагрузить ноды, чтобы создался systemd mount.
Настройка keepalived
Для настройки keepalived был написан скрипт, который выполняет следующие действия:
подключается по ssh на каждую ноду и выполняет установку keepalived;
берет автоматически наименование сетевого интерфейса;
генерирует конфигурационный файл keepalived;
запускает на всех 3 нодах службу keepalived;
для дальнейшей установки временно приостанавливает службу на 2 и 3 нодах.
Патчинг инсталлятора продукта
Так как инсталлятор продукта представляет из себя набор bash-скриптов, что очень удобно в плане доработки многих моментов, до момента установки можно адаптировать некоторые из них под дальнейшее переключение продукта в Swarm. Для этого были подготовлены скрипты и docker-compose.yml файлы для некоторых сервисов, которые разворачивают сервис в совместимом с Compose и Swarm виде. Эти файлы просто заменяются в директории инсталлятора.
Установка продукта
Перед непосредственно установкой продукта выполняется генерация скриптов для подготовки внешней СУБД (создание нужных ролей, БД и связей). Затем производится установка по стандартному сценарию.
Установка registry
Для установки registry был написан скрипт, который выполняет следующие действия:
генерирует ssl-сертификат для TLS соединения с registry;
добавляет данный сертификат в доверенные на каждой ноде;
создает сервис registry;
берет контейнеры, запущенные с помощью
docker-compose.ymlфайлов, находящихся в директории/opt/colibri. Образы этих контейнеров тегирует в соответствии с адресом реестра;загружает в реестр протегированные образы.
Выравнивание состояния нод
Для некоторых сервисов создаются пользователи в хостовой системе, а также директории для логов внутри /var/log/colibri. Для выравнивания этого состояния также написан скрипт, который приводит состояние 2 и 3 нод к идентичному первой ноде.
Переключение в Swarm
Далее необходимо переключить сервисы из Compose в Swarm для этого нужна два компонента:
подготовленные
docker-compose.ymlиdocker-init.sh, которые были написаны в процессе настройки продукта под Swarm-развертывание;скрипты по остановке Compose-сервисов и применению изменений с последующим запуском в Swarm.
Для удобства первый скрипт отключает все сервисы в пути до docker-compose.yml содержится /opt/colibri. А второй скрипт берет подготовленные файлы, распаковывает по нужным путям, подставляя необходимые значения, затем делает деплой сервиса.
Стадия 6. Тестирование
После того, как работа скриптов была отполирована, установка проходит без проблем. Можно отдавать на проверку сам продукт. Если взять проблемы, которые могут возникнуть после переноса, самые частые приведу ниже:
ошибки по правам – они фиксятся проверкой uid владельца файла или добавлением недостающих capability;
ошибки по сетевой связности – высокая вероятность того, что где-то в конфигурационных файлах указали неверный порт или адрес сервиса, к которому идет обращение.
Сложности
-
В блок проблем нужно описать проблему с использованием корневого раздела для GlusterFS
При использовании GlusterFS обнаружилась интересная особенность. В продукте используется Grafana и по умолчанию она пишет все в файловую БД sqlite. Механизмы блокировок, используемые sqlite, приводили к тому, что том GlusterFS, на котором располагалась БД Grafana, уходил в аварийное состояние, и диск отмонтировался от хоста.
Для решения этой проблемы БД Grafana перенесли на PostgreSQL. Также в начале переноса отдельный диск для GlusterFS не выделялся и делился на разделы основной диск-системы, что при возникновении проблемы с БД sqlite уводил файловую систему всей виртуальной машины в read-only.
-
Высокодоступность работает, но с одним нюансом. Настройка консьюмеров для работы с rabbitmq
Перенеся продукт в Swarm, отследил такой кейс: если по какой-либо причине контейнер с rabbitmq завершает свою работу с ошибкой и переподнимается, контейнеры с .NET-приложениями теряют связь с очередью, и их необходимо тоже перезапускать. Для решения этой проблемы нужно, чтобы на стороне консьюмера было настроено переподключение к сервису очереди.
-
Сервис-одиночка, который не попал в кластер...
В процессе тестирования возникла трудность: контейнер с tftpd не отдавал контент, когда находился за балансировщиком Swarm. Причину такого поведения определить не удалось, и решили разворачивать 1 реплику контейнера на каждой ноде в Compose.
-
Проблема параллельной разработки решения мононоды и кластера
При подготовке Swarm-реализации продукта произошла ситуация, когда основная ветка продукта ушла на релиз вперед, а процесс подготовки занимает время. Здесь нужно внести изменения в методологию разработки, чтобы изменения по контейнерам были максимально Swarm-совместимыми и был параллельный стенд, на котором тестируется развертывание. Это нужно для минимизации времени подготовки релиза, включающего в себя оба режима работы продукта.
-
Проблема редеплоя сервисов в Docker Swarm, и при чем тут nginx
Процесс запуска nginx включает в себя этап резолва DNS-имен для upsteam, proxy_pass, fastcgi_pass, после которого он в конфигурации хранит IP-адреса и, если в процессе работы у DNS-имени поменялся IP-адрес, nginx ничего об этом не будет знать.
Эта проблема проявляется, если, например, редеплоить сервис. Сначала из конфигурации swarm удаляется сервис, затем заново добавляется, и у него уже новый IP-адрес. В таких ситуациях nginx начинает отдавать код ответа 502. Чтобы решить это, нужно перезагрузить nginx.
Тут же при установке продукта, когда добавляется новый компонент, которому нужно реверс-прокси от nginx, выполняется цепочка действий с добавлением новых конфигурационных файлов и перезапуска nginx. Чтобы решить эти проблемы, была сделана надстройка над nginx, которая слушает события docker, затем делает резолв и healthcheck сервиса, который добавился в стек продукта внутри Swarm – и, если все нормально, генерирует из шаблона конфигурационный файл nginx и делает его перезагрузку.
6. Фикс ошибок на площадках нужно сразу дублировать в скрипты
После того, как рабочая протестированная версия продукта стала готовой и доступной, она сталкивается с тестовым или продуктивным внедрением. На этом этапе продукт может испытывается в условиях, которые не всегда возможно предугадать. Это могут быть инфраструктурные особенности и ограничения, требования служб ИБ, пожелания заказчика, которые целесообразно выполнить.
В такие моменты могут появляться ситуации, где требуется что-то "доработать напильником:" важно в такие моменты правильно выстроить процесс анализа и внесения изменений. Все, что находится, необходимо дублировать у себя в системах контроля версий или базах знаний, или при невозможности фиксировать отдельно, чтобы потом донести это в базу знаний или код. Иначе очень высок риск сталкиваться с одними и теми же ошибками на разных площадках.
7. Отсутствие решения для порядка запуска для разрозненных docker-compose.yml
Как писал ранее, чтобы в Compose или Swarm задать порядок запуска сервисов, необходимо, чтобы все сервисы были описаны в едином docker-compose.yml файле. Для больших проектов это усложняет поддержку этого файла.
В случае c Compose мы находимся на стадии обсуждения и тестирования возможных сценариев решения задачи. В случае со Swarm ситуация проще, потому что он будет перезапускать сервис достаточно долго, чтобы сервисы успели запуститься и быть готовыми принимать соединения.
8. Разные операционные системы для серверов у заказчиков
Отдельно отмечу работу с операционными системами. Как правило, у заказчиков есть определенный пул ОС, которые они могут использовать внутри собственной инфраструктуры для развертывания серверов. Для того, чтобы не поддерживать множество версий продукта под каждую ОС, продукт был вынесен в контейнеры – поскольку данный уровень абстракции убирает критерий совместимости продукта с ОС, и в данном случае для работы требуется установленный Docker.
Однако с этим все равно возникают свои нюансы, хоть и Docker достаточно распространен, в каких-то ОС есть свои доработанные реализации Docker-движка. Например, в Astra Linux в состав их пакета Docker идет утилита openscap, которая сканирует образы контейнеров на уязвимости, и данная утилита может просто не дать вам развернуть контейнер на системе. И при возникновении разных подобных особенностей работы ОС и docker в частности то и дело выпадает необходимость работать с вендором.
Итоги
Как итог – был выполнен процесс переноса продукта в Swarm-кластер с выполнением задачи обеспечения высокой доступности продукта.
Из дополнительного ПО было добавлено GlusterFS как распределенное файловое хранилище, keepalived – для обеспечения единой точки входа, registry – как источник образов контейнеров для кластера, разработанная собственными силами надстройка над nginx для выполнения задачи service discovery для проксируемых сервисов.
Сама задача была интересна как на этапе появления, так и в процессе решения. Текущий результат не итоговый, а первый полностью функциональный.
Сталкиваясь неоднократно с особенностями Docker Swarm в плане различий с Compose, то и дело хотел узнать и сравнить, как, используя популярный Kubernetes, можно достичь того же результата, с какими трудностями придется столкнуться, какие задачи решить.
Для обеспечения высокой доступности, был выбран очевидный, простой и достаточный путь. Был подготовлен платформенный слой, а затем необходимый минимум по развертыванию приложения в высокодоступной конфигурации на данной платформе.
Есть нюансы при попытке обеспечить высокую доступность продукта, который изначально не разрабатывался с прицелом на подобные условия работы. Для решения этих нюансов требовалось вносить доработки. В данном кейсе этими нюансами стали: выбор стека платформы, исходя из контекста использования продукта и компетенций, а также дополнительная работа по подготовке конфигурационных файлов всех сервисом для запуска на этой платформе. Следующим этапом планируется полная интеграция Swarm-решения с инсталлятором, чтобы избавиться от отдельного тулинга, и такая конфигурация была доступна "из коробки".