Настройка MongoDB ShardedCluster с X.509 аутентификацией

в 10:33, , рубрики: cluster, mongodb, mongoengine, pymongo, python, replica set, X.509, Администрирование баз данных, хранение данных
Комментарии к записи Настройка MongoDB ShardedCluster с X.509 аутентификацией отключены

Всем доброго времени суток! Недавно жизнь подкинула автору увлекательную работу по развертыванию MongoDB кластера с настройкой репликации и шардирования, а также аутентификации c использованием x.509 сертификатов. В данной статье я в первую очередь хотел бы изложить свои мысли и поделиться полученными опытом. Так как некоторые вещи оказались не тривиальными и сделать их с первого раза не удавалось, то думаю мои пошаговые инструкции могут пригодиться для освещения вопроса тем кто только знакомится с шардированием данных и работой с MongoDB в целом.
Также я буду очень рад увидеть рекомендации по добавлению/изменению конфигурации кластера и просто вопросы или критику по самой статье или по сути вопроса.

Вступление

Проект в рамках которого производилось внедрение кластера представляет из себя сервис по сбору статистки на устройствах клиентов и агрегированного ее предоставления на сайте или посредством Rest-API. Проект долгое время стабильно работал под низкой нагрузкой и как следствие сервер MongoDB, установленный как есть “из коробки” (без шардирования и репликации данных) отлично справлялся с возложенной на него целью, а “спокойный сон” обеспечивали ежедневные бекапы базы по крону. Гром грянул как обычно в один прекрасный момент после прихода нескольких больших клиентов с большим количеством устройств, данных и запросов. Следствием стало недопустимо долгое выполнение запросов к подросшей БД, и кульминацией стало нарушение работы сервера, когда мы чуть не потеряли данные.

Таким образом в одночасье встала необходимость выполнить работы по повышению отказоустойчивости, сохранности данных, производительности с возможностью будущего масштабирования. Было решено использовать имеющийся потенциал MongoDB для устранения возникших проблем, а именно — организовать шардированый кластер с репликацией и мигрировать на него имеющиеся данные.

Немного теории

Для начала немого разберемся с ShardedCluster MongoDB и его основными компонентами. Шардирование как таковое является методом горизонтального масштабирования вычислительных систем, хранящих и предоставляющих доступ к данным. В отличие от вертикального масштабирования, когда производительность системы удается увеличить за счет повышения производительности отдельно взятого сервера, например, за счет перехода на более мощный CPU, добавления объема доступной оперативной памяти или дискового пространства, шардирование работает за счет распределения набора данных и нагрузки между несколькими серверами и добавления новых серверов по мере необходимости (это как раз наш случай).

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

Что ожидается получить от перехода на шардированный кластер MongoDB? В первую очередь необходимо получить распределение нагрузки операций чтения/записи между шардами кластера, а во-вторых достичь высокой отказоустойчивости (постоянная доступность данных) и сохранности данных за счет избыточного копирования (репликации).

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

Шардирование для всех баз и коллекций по-умолчанию отключено, и мы не можем шардировать системные базы кластера, такие как admin и config. При попытке сделать это мы получим от монги однозначный отказ:

mongos> sh.enableSharding("admin")
{ "ok" : 0, "errmsg" : "can't shard admin database" }

Шардированый кластер MongoDB предъявляет нам три обязательных условия: собственно, наличие в нем шардов, все общение между кластером и его клиентами должно осуществляться исключительно посредством роутеров mongos и в кластере должен присутствовать конфиг-сервер (на основе дополнительного инстанса mongod, либо как рекомендуется на основе Реплика Сета).

В официальной документации mongodb сказано “In production, all shards should be replica sets.”. Будучи репликой (Replica Set) каждый шард за счет множественного копирования данных повышает свою отказоустойчивость (в плане доступности данных на любом инстансе реплики) ну само собой обеспечивает лучшую их сохранность.

Реплика (Replica Set) представляет из себя объединение нескольких запущенных экземпляров mongod, которые хранят копии одного и того же набора данных. В случае с шард-репликой это будет набор чанков переданный данному шарду балансировщиком монги.

Один из экземпляров реплики назначается главным (PRIMARY) и принимает все операции записи данных (при этом поддерживая и чтение), остальные монгоды в таком случае объявляются SECONDARY и в асинхронном общении с PRIMARY актуализируют свою копию набора данных. Они также доступны для чтения данных. Как только PRIMARY по каким-либо причинам становится недоступным, переставая взаимодействовать с остальными участниками реплики, среди всех доступных участников реплики объявляется голосование на роль нового PRIMARY. На самом деле кроме PRIMARY и SECONDARY в Replica Set может быть еще третий вид участников — это арбитр (ARBITER).

Арбитр в реплике не выполняет роль копирования набора данных, вместо этого он решает важную задачу голосования и призван оградить реплику от тупикового исхода голосования. Представьте ситуацию, когда в реплике четное количество участников и они пополам голосуют за двух претендентов с одинаковым итоговым количеством голосов, и так бесконечно… Добавив в такую “четную” реплику арбитра, он решит исход голосования, отдав свой голос, тому или иному претенденту на “должность” PRIMARY, не требуя при этом ресурсов на обслуживание еще одной копии набора данных.

Замечу, что Replica Set — это объединение именно экземпляров mongod, то есть ничто не мешает вам собрать реплику на одном сервере, указав в качестве хранилищ данных папки, находящиеся на разных физических носителях, и добиться этим некоторой безопасности данных, но все же идеальный вариант — это организация реплики с запуском mongod на разных серверах. Вообще в этом отношении система MongoDB очень гибкая, и позволяет собрать необходимую нам конфигурацию исходя из наших потребностей и возможностей, не предъявляя жестких ограничений. Replica Set как таковая вне контекста Sharded Cluster является одной из типовых схем организации сервера MongoDB, которая дает на выходе высокую степень отказоустойчивости и защиты данных. В таком случае каждый участник реплики хранит полную копию всего набора данных базы, а не его часть, определенную набором чанков шарда.

Инфраструктура

Приводимая ниже конфигурация кластера построена на трех виртуальных контейнерах (VBO) OpenVZ. Каждая из виртуалок расположена на отдельном выделенном сервере.

Две виртуальные машины (далее server1.cluster.com и server2.cluster.com) имеют больше ресурсов — на них ляжет обязанность репликации, шардирования и предоставление данных клиентам. Третья машина (server3.cluster.com) имеет более слабую конфигурацию — ее назначение обеспечение работы экземпляров mongod-арбитров.

В построенном кластере сейчас у нас сейчас три шарада. В нашей схеме мы выдержали рекомендацию построения шардов на основе реплика сетов, но с некоторым допущением. В каждом шарде-реплика сете нашего кластера есть свой PRIMARY, SECONDARY и ARBITER, работающие на трех разных серверах. Также есть конфиг-сервер построенный также с применением репликации данных.

Однако серверов у нас всего три, один из которых не выполняет функции репликации данных (только в случае конфиг-реплики) и поэтому все три шарда фактически располагаются на двух серверах.

На схемах из документации монги, монгосы изображены на серверах приложений. Я решил нарушить это правило и разместить монгосы (у нас их будет два) на серверах данных: server1.cluster.com и server2.cluster.com, избавившись от дополнительной настройки mongodb на серверрах приложений и ввиду определенных ограничений связанных с серверами приложений. Сервера приложений имеют возможность подключаться к любому из двух монгосов, таким образом в случае проблем с одним из и них они после непродолжительного тайм-аута переподключатся к другому. Сервера приложений в свою очередь сидят за DNS-ом на котором настроен Round Robin. Он поочередно выдает один из двух адресов, обеспечивая примитивную балансировку подключений (запросов клиентов). В планах заменить его каким-нибудь “умным” DNS (возможно кто-то подскажет хорошее решение в комментариях, буду благодарен!) для выдачи нужного сервера по георасположению клиента.

Для ясности привожу общую схему сформированного кластера с названиями серверов и запущенными на них приложениями. Через двоеточия указаны назначенные порты приложений.

Настройка MongoDB ShardedCluster с X.509 аутентификацией - 1

Первичная настройка

Зайдем на server1.cluster.com и установим последнюю версию пакета MongoDB Community Edition из официального репозитария. На момент сборки кластера — это версия 3.2.8. В моем случае на всех машинах кластера установлена операционная система Debian 8, подробную инструкцию по установке на свою ОС вы можете найти в официальной документации.
Импортируем в систему публичный ключ, обновляем списки пакетов и устанавливаем сервер mongodb с набором утилит:

server1.cluster.com:~# apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927

server1.cluster.com:~# echo "deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/3.2 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list

server1.cluster.com:~# apt-get update
server1.cluster.com:~# apt-get install -y mongodb-org

Готово! В результате выполненных действий получаем на своей машине MongoDB сервер, который уже запущен и работает. Пока отключим сервис mongod (мы еще вернемся к нему):

server1.cluster.com:~# service mongod stop

Далее создаем каталог, в котором будем хранить все данные нашего кластера, у меня он располагается по пути “/root/mongodb”. Внутри формируем следующую структуру каталогов:

.
├── cfg
├── data
│   ├── config
│   ├── rs0
│   ├── rs1
│   └── rs2
├── keys
└── logs

В подкаталоге data у нас будут храниться непосредственно данные наших реплик (в т.ч. конфиг-реплики). В cfg мы создадим фалы конфигураций для запуска необходимых экземпляров mongo{d/s}. В keys мы скопируем ключи и сертификаты для x.509 аутентификации участников кластера. Назначение папки logs, думаю всем понятно.

Аналогично процедуру с установкой и каталогами нужно повторить на оставшихся двух серверах.

Прежде чем перейти к настройке и связыванию компонентов нашего кластера убедимся, что все работает как нам необходимо. Запустим экземпляр mongod на порту 27000, указав каталог под данные в “/root/mongodb/data/rs0”:

mongod --port 27000 --dbpath /root/mongodb/data/rs0

На том же сервере откройте еще один терминал и подключитесь к запущенному монгоду:

mongo --port 27000

Если все прошло успешно мы попадем в shell mongodb и можем выполнить пару команд. По-умолчанию монга переключит нас на тестовую базу данных, в этом мы можем убедиться, введя команду:

> db.getName()
test

Удалим ненужную нам БД командой:

> db.dropDatabase()
{ "ok" : 1 }

И проинициализируем новую БД с которой будем экспериментировать переключившись на нее:

> use analytics
switched to db analytics  

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

Добавим пару устройств:

> db.sensors.insert({'s':1001, 'n': 'Sensor1001', 'o': true, 'ip': '192.168.88.20', 'a': ISODate('2016-07-20T20:34:16.001Z'), 'e': 0})
WriteResult({ "nInserted" : 1 })
> db.sensors.insert({'s':1002, 'n': 'Sensor1002', 'o': false, 'ip': '192.168.88.30', 'a': ISODate('2016-07-19T13:40:22.483Z'), 'e': 0})
WriteResult({ "nInserted" : 1 })

Здесь,
s – порядковый номер сенсора;
n – его строчный идентификатор;
o – текущий статус (online/offline);
ip – ip адрес сенсора;
a – время последней активности;
e – признак наличия ошибки;

А теперь несколько записей статистических данных вида:

> db.statistics.insert({'s':1001, ‘ts’: ISODate('2016-08-04T20:34:16.001Z'), ‘param1’: 123, ‘param2’: 23.45, ‘param3’: “OK”, ‘param4’: True, ‘param5’: ‘-1000’, ‘param6’: [1,2,3,4,5])
WriteResult({ "nInserted" : 1 })

s – номер сенсора;
ts – TimeStamp;
param1..param6 – некоторые статистические данные.

Клиенты сервиса статистической аналитики часто выполняют агрегированные запросы чтобы получить некоторые репрезентативные данные по собранной со своих устройств статистике. Практически во всех запросах участвует “порядковый номер сенсора” (поле s). К нему часто применяются сортировки и группировка, поэтому для оптимизации (а также для шардинга) добавим в коллекцию statistics индекс:

mongos> db.statistics.ensureIndex({"s":1})

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

Аутентификация с использованием x.509 сертификатов

Чтобы понять задачу, немого забежим вперед и представим запущенные на разных серверах экземпляры mongod, которые необходимо объединить в реплику, подключить к ним mongos и предусмотреть возможность безопасного подключения клиентов к сформированному кластеру. Само собой, участники обмена данными должны проходить проверку подлинности при подключении (быть доверенными) и желательно, что бы канал передачи данных также был защищен. На этот случай у MongoDB имеется поддержка TSL/SSL, а также несколько механизмов аутентификации. Одним из вариантов установления доверительных отношений между участниками обмена данными в кластере, является использование ключей и сертификатов. По поводу выбора механизма, использующего этот вариант в документации монги есть рекомендация:

“Keyfiles are bare-minimum forms of security and are best suited for testing or development environments. For production environments we recommend using x.509 certificates.”

X.509 является стандартом ITU-T для инфраструктуры открытых ключей и управления привилегиями. Этот стандарт определяет формат и то как распределяются открытые ключи с помощью подписанных цифровых сертификатов. Сертификат связывает открытый ключ с некоторым субъектом — пользователем сертификата. Достоверность этой связи достигается за счет цифровой подписи, которую выполняет доверенный центр сертификации.

(Помимо x.509 в MongoDB есть также высоконадежные методы Enterprise уровня – это Kerberos Authentication и LDAP Proxy Authority Authentication), но это не наш случай и здесь будет рассмотрена настройка именно x.509 аутентификации.

Механизм аутентификации с использованием x.509 сертификатов требует защищенного TSL/SSL подключения к кластеру, которое включается соответствующим аргументом запуска mongod --sslMode, либо параметром net.ssl.mode в файле конфигурации. Проверка подлинности клиента, подключившегося к серверу в таком случае сводится к проверке подлинности сертификата, а не логина и пароля.

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

Чтобы выполнить условия x.509 нам необходим единый ключ – так называемый “Центр сертификации” Certificate Authority (CA). На его основе будут выдаваться как клиентские, так и сертификаты участников кластера, поэтому в первую очередь создадим секретный ключ для нашего CA. Правильно будет выполнять все следующие действия и хранить секретные ключи на отдельной машине, но в данной статье я буду выполнять все действия на первом сервере (server1.cluster.com):

server1.cluster.com:~/mongodb/keys# openssl genrsa -out mongodb-private.key -aes256
Generating RSA private key, 2048 bit long modulus
.....................+++
........................................................+++
e is 65537 (0x10001)
Enter pass phrase for mongodb-private.key:
Verifying - Enter pass phrase for mongodb-private.key:

На предложение ввести секретную фразу вводим и подтверждаем какую-нибудь надежную комбинацию, например, “temporis$filia$veritas” (у вас конечно-же будет что-то свое и более сложное). Фразу нужно обязательно запомнить, она понадобится нам для подписи каждого нового сертификата.

Далее мы создаем CA сертификат (сразу после запуска команды нас попросят ввести секретную фразу от ключа, который мы указали (в параметре “key”):

server1.cluster.com:~/mongodb/keys# openssl req -x509 -new -extensions v3_ca -key mongodb-private.key -days 36500 -out mongodb-CA-cert.crt

Обращу ваше внимание на параметр days – он отвечает за время действия сертификата. Я не уверен в том кто и через сколько будет заниматься проектом, над которым в данный момент работаю, поэтому дабы исключить неприятные сюрпризы – указываем сертификату 36500 дней жизни, что соответствует 100 годам (очень оптимистично, не правда ли?).
Пройдя проверку фразы нас попросят ввести информацию об организации-владельце сертификата. Представим, что наша крупная организация называется “SomeSysyems” и располагается городе Москва (вводимая информация следует после двоеточий):

Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: Statistics
Common Name (e.g. server FQDN or YOUR name) []: CaServer
Email Address []: info@SomeSystems.com

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

Процедура создания сертификатов для участников кластера (сертификаты для внешних клиентов будут рассмотрены отдельно) выглядит следующим образом:

  1. Мы генерируем приватный ключ (*.key — файл) и “запрос на получение сертификата” (csr-файл). CSR (Certificate Signing Request) – это текстовый файл который содержит в закодированном виде информацию об организации выдавшей сертификат и публичный ключ.
  2. Используя секретный ключ и публичный сертификат нашего Центра сертификации, подписываем сертификат для текущего сервера.
  3. Из нового ключа и сертификата участника кластера формируем PEM-файл, который используем для подключения к кластеру.

Создаем приватный ключ и запрос на получение сертификата для нашего первого сервера (server1.cluster.com). Обращу внимание на важную деталь, при заполнении все поля остаются такими же как для корневого сертификата, за исключением CN (Common Name). Его необходимо сделать уникальным для каждого сертификата. В нашем случае в качестве значения будет указываться полное доменное имя — FQDN (Fully Qualified Domain Name) конкретного сервера:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout server1.key -out server1.csr

Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: Statistics
Common Name (e.g. server FQDN or YOUR name) []: server1.cluster.com
Email Address []: info@SomeSystems.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Extra-поля я оставил пустыми. Если вы решите указать дополнительно пароль (A challenge password []:), то в конфигурации mongod вам нужно будет указать пароль к данному сертификату за который отвечают параметры net.ssl.PEMKeyPassword и net.ssl.clusterPassword. (Подробности по этим параметрам в документации здесь).

Далее мы подпишем CSR файл нашим CA сертификатом и получим публичный сертификат (файл *.crt):

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in server1.csr -out server1.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=Statistics/CN=server1.cluster.com/emailAddress=info@SomeSystems.com
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

Теперь нам нужно сделать PEM-файл:

server1.cluster.com:~/mongodb/keys# cat server1.key server1.crt > server1.pem

PEM-файл мы будем использовать непосредственно при запуске экземпляров mongod, и укажем в конфигруации.
Теперь необходимо повторить операцию по созданию сертификата для оставшихся серверов. Для полного понимания привожу все команды:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout server2.key -out server2.csr

Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: Statistics
Common Name (e.g. server FQDN or YOUR name) []: server2.cluster.com
Email Address []: info@SomeSystems.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

(extra-поля не заполнялись)

Подписываем CSR файл нашим CA сертификатом для получения публичного сертификата (файл *.crt) второго сервера:

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in server2.csr -out server2.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=Statistics/CN=server2.cluster.com/emailAddress=info@SomeSystems.com
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

Теперь нам нужно сделать PEM-файл:

server1.cluster.com:~/mongodb/keys# cat server2.key server2.crt > server2.pem

И аналогично для сертификата третьего сервера:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout server3.key -out server3.csr

Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: Statistics
Common Name (e.g. server FQDN or YOUR name) []: server3.cluster.com
Email Address []: info@SomeSystems.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

(extra-поля не заполнялись)

Подписываем CSR файл нашим CA сертификатом для получения публичного сертификата (файл *.crt) третьего сервера:

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in server3.csr -out server3.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=Statistics/CN=server3.cluster.com/emailAddress=info@SomeSystems.com
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

Создаем PEM-файл:

server1.cluster.com:~/mongodb/keys# cat server3.key server3.crt > server3.pem

Повторю, что все ключи и сертификаты были созданы мною на первом сервере и затем по необходимости перемещены на соответствующий сервер. Таким образом на каждом из трех серверов должен оказаться публичный CA сертификат (mongodb-CA-cert.crt) и PEM-файл этого сервера (server<$N>.pem).

Конфигурация экземпляров mongod

Для корректного запуска нам необходимо передать ряд параметров экземплярам mongod. Для этого можно использовать конфигурационный файл, либо передавать все необходимые значения в качестве аргументов терминальной команды. Практически все параметры конфигурации имеют свое отражение в соответствующих аргументах командной строки. Вариант с конфиг-файлом на мой взгляд более оправдан, так как отдельный структурированный файл проще читать и дополнять, и в таком случае запуск, экземпляра программы сводится к передаче ему единственного аргумента – расположения конфигурационного-файла:

mongod --config <path-to-config-file>

Итак, создадим файл конфигурации для экземпляра mongod первой шард-реплики (rs0) на первом сервере:

#
# /root/mongodb/cfg/mongod-rs0.conf
#

replication:
 replSetName: "rs0" # название реплики

net:
 port:      27000
 ssl:
      mode:       requireSSL # требуем защищенного соединения
      PEMKeyFile:  /root/mongodb/keys/server1.pem
      clusterFile: /root/mongodb/keys/server1.pem
      CAFile:     /root/mongodb/keys/mongodb-CA-cert.crt
      weakCertificateValidation: false # запрещаем подключаться без сертификата
      allowInvalidCertificates:  false # запрещаем подключение с невалидными сертификатами

security:
 authorization:   enabled # требуем обязательную авторизацию
 clusterAuthMode: x509 # метод авторизации - MONGODB-X509

storage:
 dbPath :   /root/mongodb/data/rs0 # указываем каталог для данных

systemLog:
  destination: file # будем выводить лог в файл
  path:     /root/mongodb/logs/mongod-rs0.log # путь для лог-файла
  logAppend:   true # дописывать лог-файл при следующем запуске

Аналогичный файл создаем для второй шард-реплики (rs1), но изменяем порт, название реплики, расположение каталога данных и лог файла:

#
# /root/mongodb/cfg/mongod-rs1.conf
#

replication:
 replSetName: "rs1"

net:
 port:      27001
 ssl:
      mode:       requireSSL
      PEMKeyFile:  /root/mongodb/keys/server1.pem
      clusterFile: /root/mongodb/keys/server1.pem
      CAFile:     /root/mongodb/keys/mongodb-CA-cert.crt
      weakCertificateValidation: false
      allowInvalidCertificates:  false

security:
 authorization:   enabled
 clusterAuthMode: x509

storage:
 dbPath :   /root/mongodb/data/rs1

systemLog:
  destination: file
  path:     /root/mongodb/logs/mongod-rs1.log
  logAppend:   true

И по аналогии для третьей реплики (rs2):

#
# /root/mongodb/cfg/mongod-rs2.conf
#

replication:
 replSetName: "rs2"

net:
 port:      27002
 ssl:
      mode:       requireSSL
      PEMKeyFile:  /root/mongodb/keys/server1.pem
      clusterFile: /root/mongodb/keys/server1.pem
      CAFile:     /root/mongodb/keys/mongodb-CA-cert.crt
      weakCertificateValidation: false
      allowInvalidCertificates:  false

security:
 authorization:   enabled
 clusterAuthMode: x509

storage:
 dbPath :   /root/mongodb/data/rs2

systemLog:
  destination: file
  path:     /root/mongodb/logs/mongod-rs2.log
  logAppend:   true

Помимо инстансов, организующих три шард-реплики в нашем кластере будут монгоды обеспечивающие работу сервера конфигурации, который будет построен на основе реплики (rscfg).

Стоит пояснить, что роль конфиг-сервера может выполнять и один mongod (как впрочем и в случае с шардом), но для обеспечения надежности и отказоустойчивости рекомендуется делать конфиг-сервер также на основе Replica Set.

Конфиг-файл служебной реплики отличается от реплик данных наличием параметра “sharding.clusterRole” который сообщает инстансу mongod его особое предназначение:

#
# /root/mongodb/cfg/mongod-rscfg.conf
#

sharding:
  clusterRole: configsvr # указываем роль в кластере - сервер конфигурации

replication:
 replSetName: "rscfg" # название реплики

net:
 port:      27888
 ssl:
      mode:       requireSSL
      PEMKeyFile:  /root/mongodb/keys/server1.pem
      clusterFile: /root/mongodb/keys/server1.pem
      CAFile:     /root/mongodb/keys/mongodb-CA-cert.crt
      weakCertificateValidation: false
      allowInvalidCertificates:  false

security:
 authorization:   enabled
 clusterAuthMode: x509

storage:
 dbPath :   /root/mongodb/data/config

systemLog:
  destination: file
  path:     /root/mongodb/logs/mongod-rscfg.log
  logAppend:   true

Теперь нам необходимо скопировать все созданные конфигурационные файлы на остальные серверы. После копирования не забываем изменить значения в параметрах net.ssl.PEMKeyFile и net.ssl.clusterFile в которых должны быть указаны сертификаты соответствующего сервера (server2.pem, server3.pem).

Настройка Replica Set

На первом сервере запустим mongod на порту 27000, без указания “боевого” конфигурационного файла – только порт и каталог данных. Это делается для того чтобы запускаемый экземпляр mongod пока не считал себя участником реплики и также не предъявлял строгих требований к установлению подключения и аутентификации, которые мы указали в конфигурационных файлах:

mongod --port 27000 --dbpath /root/mongodb/data/rs0

Далее нам необходимо подключиться к запущенному монгоду и добавить суперпользователя будущей реплики, для того чтобы в дальнейшем, после включения авторизации, указанной в нашем конфиг-файле, у нас были права на изменение реплики, в том числе на первичную инициализацию. Как показала практика включение x.509 авторизации не запрещает нам добавлять в БД традиционных юзеров (проходящих авторизацию по логину и паролю). Тем не менее я решил не прибегать к данной возможности, а использовать x.509 механизм повсеместно как на уровне кластера, так и при формировании реплик. Чтобы было понятно скажу, что пользователь, которого мы сейчас создадим, является пользователем уровня данной реплики. Из других реплик и на уровне кластера он будет не доступен.

Для нового юзера нам понадобится создать еще один сертификат подобно тому как мы уже делали в разделе “x.509 аутентификация”. Отличием этого сертификата будет то, что он привязан не к участнику кластера (экземпляру mongod или серверу), а к учетной записи. Иными словами, мы создадим клиентский сертификат. Данный сертификат будет привязан к суперпользователю (роль root) реплика сета первого шарда (rs0). О встроенных ролях MongoDB, можно прочитать в данном разделе официальной документации.

Нам необходимо зайти на наш CA сервер. И сгенерировать еще один ключ и запрос на подпись сертификата:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout rsroot.key -out rsroot.csr
Generating a 2048 bit RSA private key
........................................................................+++
.........................+++
writing new private key to 'rsroot.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow      
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: StatisticsClient
Common Name (e.g. server FQDN or YOUR name) []: rsroot
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Подписываем сертификат (снова понадобится секретная фраза от СA ключа):

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in rsroot.csr -out rsroot.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=StatisticsClient/CN=rsroot
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

Создаем PEM-файл:

server1.cluster.com:~/mongodb/keys# cat rsroot.key rsroot.crt > rsroot.pem

Обращу ваше внимание на параметр Organisation Unit Name (OU), а именно на то, что при генерации клиентских сертификатов он должен быть отличным от того который мы указывали при генерации сертификатов Участников кластера. В противном случае при добавлении в кластер юзера, содержащего subject (объясняется ниже) с OU равным тому, что имеют в своих сертификатах участники кластера, монга может отказать вам с ошибкой:

{
   "ok" : 0,
   "errmsg" : "Cannot create an x.509 user with a subjectname that would be recognized as an internal cluster member.",
   "code" : 2
}

Юзер для авторизации по механизму x.509 добавляется несколько необычным образом, нам необходимо указать не его имя и пароль, а идентификатор (subject) сертификата который ему соответствует. Получить subject можно из PEM-файла выполнив команду:

server1.cluster.com:~/mongodb/keys# openssl x509 -in rsroot.pem -inform PEM -subject -nameopt RFC2253
subject= CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU
-----BEGIN CERTIFICATE-----

В выводе нас интересует содержимое строчки начинающейся на “subject=” (без самого “subject=” и пробела). Подключимся к монгоду и добавим юзера:

mongo --port 27000

> db.getSiblingDB("$external").runCommand({createUser: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "root", db: "admin"}]
})

$external – это название виртуальной базы данных используемой для создания пользователей, чьи учетные данные хранятся вне MongoDB, например, как в нашем случае (для проверки подлинности используется файл сертификата).

Теперь выйдем из шела монги и перезапустим mongod, теперь уже c соответствующим конфигурационным файлом. То же самое нужно сделать на втором и третьем серверах. Таким образом у нас должны быть запущены все монгоды первой реплики (rs0).
Подключаемся к монгоду с использованием сертификата созданного суперпользователя реплики (rsroot) и проходим аутентификацию, указав в качестве имени пользователя — subject сертификата:

server1.cluster.com:~/mongodb/keys# mongo admin --ssl --sslCAFile /root/mongodb/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb/keys/rsroot.pem --host server1.cluster.com --port 27000

> db.getSiblingDB("$external").auth({
   mechanism:"MONGODB-X509",
   user: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU"
})

Инициализируем нашу реплику:

rs.initiate(
  {
      _id: "rs0",
      members: [
      { _id: 0, host : "server1.cluster.com:27000" },
      { _id: 1, host : "server2.cluster.com:27000" },
      { _id: 2, host : "server3.cluster.com:27000", arbiterOnly: true },
      ]
  }
)

Обратите внимание на параметр arbiterOnly для третьего сервера, который мы в самом начале договаривались сделать “сервером арбитров”.

Переподключившись к монгоду, по префиксу “rs0” в шеле мы увидим, что теперь он принадлежит одноименной реплике:
rs0:PRIMARY (У вас текущий сервер может быть избран SECONDARY).

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

1. Запускаем монгод без конфига на первом сервере (изменился порт и каталог данных):

mongod --port 27001 --dbpath /root/mongodb/data/rs1

2. Подключаемся к запущенному монгоду и добавляем суперуюзера реплики (rs1). Я буду использовать один и тот же сертификат для всех реплик, поэтому subject используется такой же как у первой реплики:

mongo --port 27001

> db.getSiblingDB("$external").runCommand({createUser: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "root", db: "admin"}]
})

3. Перезапустим mongod на первом сервере, указав конфигурационный файл. На втором и третьем серверах также поднимаем по монгоду с соответствующим конфигом:

root@server1.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs1.conf
root@server2.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs1.conf
root@server3.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs1.conf

4. Подключаемся к монгоду с указанием сертификата, проходим аутентификацию и инициализируем реплику rs1:

root@server1.cluster.com# mongo admin --ssl --sslCAFile /root/mongodb/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb/keys/rsroot.pem --host server1.cluster.com --port 27001

> db.getSiblingDB("$external").auth({
   mechanism:"MONGODB-X509",
   user: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU"
})

> rs.initiate(
  {
      _id: "rs1",
      members: [
      { _id: 0, host : "server1.cluster.com:27001" },
      { _id: 1, host : "server2.cluster.com:27001" },
      { _id: 2, host : "server3.cluster.com:27001", arbiterOnly: true },
      ]
  }
)

Повторяем процедуру для третьей реплики (rs2).

1. Запускаем монгод без конфига на первом сервере (не забываем сменить порт и каталог данных):

mongod --port 27002 --dbpath /root/mongodb/data/rs2

2. Подключаемся к монгоду и добавляем суперюзера реплики (rs2):

mongo --port 27002

> db.getSiblingDB("$external").runCommand({createUser: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "root", db: "admin"}]
})

3. Перезапускаем монгод на первом сервере с указанием конфигурационного файла. На втором и третьем серверах также поднимаем по монгоду с соответствующими конфигами:

root@server1.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs2.conf
root@server2.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs2.conf
root@server3.cluster.com# mongod --config /root/mongodb/cfg/mongod-rs2.conf

4. Подключаемся к монгоду с указанием сертификата, проходим аутентификацию и инициализируем реплику rs2:

root@server1.cluster.com# mongo admin --ssl --sslCAFile /root/mongodb/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb/keys/rsroot.pem --host server1.cluster.com --port 27002

> db.getSiblingDB("$external").auth({
   mechanism:"MONGODB-X509",
   user: "CN=rsroot,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU"
})

> rs.initiate(
  {
      _id: "rs2",
      members: [
      { _id: 0, host : "server1.cluster.com:27002" },
      { _id: 1, host : "server2.cluster.com:27002" },
      { _id: 2, host : "server3.cluster.com:27002", arbiterOnly: true },
      ]
  }
)

Конфиг-сервер

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

{
   "ok" : 0,
   "errmsg" : "Arbiters are not allowed in replica set configurations being used for config servers",
   "code" : 93
}

По этой причине у нас будет два SECONDARY-инстанса/монгода в конфиг-реплике. Создадим очередной сертификат для суперпользователя реплики rscfg, который как я уже сказал будет также и рутом на уровне кластера.

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout rootuser.key -out rootuser.csr
Generating a 2048 bit RSA private key
......................+++
.........................................+++
writing new private key to 'rootuser.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: StatisticsClient
Common Name (e.g. server FQDN or YOUR name) []: root
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in rootuser.csr -out rootuser.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=StatisticsClient/CN=root
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

server1.cluster.com:~/mongodb/keys# cat rootuser.key rootuser.crt > rootuser.pem
server1.cluster.com:~/mongodb/keys# openssl x509 -in rootuser.pem -inform PEM -subject -nameopt RFC2253
subject= CN=root,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU
-----BEGIN CERTIFICATE-----

1. Запускаем монгод без конфига на первом сервере:

server1.cluster.com:~/mongodb/keys# mongod --port 27888 --dbpath /root/mongodb/data/config

2. Подключаемся к монгоду и добавляем суперюзера реплики (rscfg).:

server1.cluster.com:~/mongodb/keys# mongo --port 27888

> db.getSiblingDB("$external").runCommand({createUser: "CN=root,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "root", db: "admin"}]
})

3. Перезапускаем mongod на первом сервере с указанием конфиг-файла. На втором и третьем серверах также поднимаем по монгоду с соответствующим конфигурационным файлом:

root@server1.cluster.com# mongod --config /root/mongodb/cfg/mongod-rscfg.conf
root@server2.cluster.com# mongod --config /root/mongodb/cfg/mongod-rscfg.conf
root@server3.cluster.com# mongod --config /root/mongodb/cfg/mongod-rscfg.conf

4. Подключаемся к монгоду с указанием сертификата, проходим аутентификацию и инициализируем конфиг-реплику (rscfg):

root@server1.cluster.com# mongo admin --ssl --sslCAFile /root/mongodb/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb/keys/rootuser.pem --host server1.cluster.com --port 27888

> db.getSiblingDB("$external").auth({
   mechanism:"MONGODB-X509",
   user: "CN=root,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU"
})

> rs.initiate(
  {
      _id: "rscfg",
      members: [
      { _id: 0, host : "server1.cluster.com:27888" },
      { _id: 1, host : "server2.cluster.com:27888" },
      { _id: 2, host : "server3.cluster.com:27888" }
      ]
  }
)

Наш конфиг-сервер на основе реплика сета готов. Теперь можно приступить к запуску mongos и подключению к кластеру.

Конфигурирование и запуск mongos

Целью монгоса является предоставление точки доступа к данным кластера (причем обращение клиентов к данным кластера допускается только через mongos). На схемах из документации MongoDB монгосы изображены запущенными на серверах приложений. В представляемой мной структуре кластера работает два экземпляра mongos, которые запущенны непосредственно на серверах server1.cluster.com и server2.cluster.com.

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

Основным отличием настроек mongos от mongod является то, что у монгосов нет каталога данных, так как они не хранят, а лишь проксируют данные. Всю необходимую информацию о конфигурации и состоянии кластера монгосы получают из коллекции config конфиг-сервера. О том как можно подключиться к конфиг-серверу монгос узнает через параметр sharding.configDB. Так как у нас конфиг-сервер построен на основе реплика сета, то и указываем мы его в формате реплики: название самой реплики, слеш и следом список хостов с их портами через запятую. Монгосы мы будем запускать на дефолтном порту монги — 27017.

#
# /root/mongodb/cfg/mongos.conf
#

sharding:
 configDB: "rscfg/server1.cluster.com:27888,server2.cluster.com:27888,server3.cluster.com:27888"

net:
 port:      27017
 ssl:
      mode:             requireSSL
      PEMKeyFile:       /root/mongodb/keys/server1.pem
      clusterFile:      /root/mongodb/keys/server1.pem
      CAFile:           /root/mongodb/keys/mongodb-CA-cert.crt
      weakCertificateValidation: false
      allowInvalidCertificates:  false

security:
 clusterAuthMode: x509

systemLog:
  destination: file
  path:     /root/mongodb/logs/mongos.log
  logAppend:   true

Копируем конфигурационный файл на оба сервера (указав соответствующие PEM-сертификаты) и запускаем в командой:

mongos --config  /root/mongodb/cfg/mongos.conf

Проверим правильность наших действий – подключимся к mongos и пройдем аутентификацию с сертификатом юзера root, которого мы добавили в конфиг-реплику (помним, что пользователь конфиг-реплики – это пользователь кластера).

mongo admin --ssl --sslCAFile /root/mongodb/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb/keys/rootuser.pem --host server1.cluster.com --port 27017

по надписи “mongos> ” видим к кому мы подключились, значит все OK.

mongos> db.getSiblingDB("$external").auth({
   mechanism:"MONGODB-X509",
   user: "CN=root,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU"
})

(ожидаем увидеть утвердительное “1” в выводе)

Вообще монга “не любит“ когда к ней подключаются из под root и в таком случае оповестит вас о том, что в целях безопасности так не стоит делать. Поэтому при работе с реальным кластером рекомендую также добавить пользователя (естественно с отдельным сертификатом) наделенного встроенной ролью userAdminAnyDatabase. Данная роль обладает практически всеми правами необходимыми для выполнения административных задач.

Думаю здесь же стоит привести пример создания сертификата еще одного пользователя. Этот пользователь будет иметь доступ только к базе analytics, и от его имени к кластеру будут подключаться все приложения нашего сервиса.

Итак, перейдем в каталог где располагаются наш Центр сертификации и создадим ключ и запрос на подпись сертификата для нового пользователя, которого мы назовем analyticsuser:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout analyticsuser.key -out analyticsuser.csr
Generating a 2048 bit RSA private key
......................+++
.........................................+++
writing new private key to 'analyticsuser.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: StatisticsClient
Common Name (e.g. server FQDN or YOUR name) []: analyticsuser
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Подписываем сертификат:

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in analyticsuser.csr -out analyticsuser.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=StatisticsClient/CN=analyticsuser
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

Создаем PEM-файл:

server1.cluster.com:~/mongodb/keys# cat analyticsuser.key analyticsuser.crt > analyticsuser.pem

Посмотрим какой subject имеет наш сертификат:

server1.cluster.com:~/mongodb/keys# openssl x509 -in rootuser.pem -inform PEM -subject -nameopt RFC2253
subject= CN=analyticsuser,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU
-----BEGIN CERTIFICATE-----

Подключимся к кластеру (монгосу) от имени пользователя с административными правами и добавим нового пользователя:

mongos> db.getSiblingDB("$external").runCommand({createUser: "CN=analyticsuser,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "readWrite", db: "analytics"}]
})

Обратите внимание мы наделили пользователя analyticsuser правами только на чтение и запись для одной базы данных analytics. Это обезопасит кластер от возможных (неосторожных или злонамеренных) действий со стороны внешних приложений к настройкам самой базы analytics и кластера в целом.

Шардинг

Шардирование в нашем случае разделит высоконагруженную коллекцию statistics по заданному индексу – ключу шардирования (Shard key) между несколькими шардами, которые мы скоро добавим. При активации шардинга для коллекции вся совокупность ее документов будет разделена на n частей, называемых чанками (Chunks). Количество чанков на которое будет разделена коллекция при включении у нее шардирования и то как часто будут формироваться новые чанки, напрямую зависит от объема данных в вашей коллекции, а также от параметра chunksize который задет размер чанка и по-умолчанию равен 64 Mb. Если вы желаете в своем кластере указать другой размер чанка, то это необходимо сделать до активации шардирования на данных коллекций, т.к. новый размер чанка будет применен только к вновь формируемым чанкам.

Для того чтобы изменить размер чанка подключимся к монгосу с сертификатом суперюзера и пройдем аутентификацию. Вообще аутентификацию можно объединить со входом, указав ее механизм (аргумент authenticationMechanism), БД которая отвечает за проверку подлинности сертификата (authenticationDatabase) и непосредственно юзера которому принадлежит сертификат (u). Для нашего суперюзера (root) команда «подключение+аутентификация” примет следующий вид:

mongo --ssl --sslCAFile /root/mongodb1/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb1/keys/rootuser.pem --host server1.cluster.com --port 27017 --authenticationMechanism "MONGODB-X509" --authenticationDatabase "$external" -u “CN=root,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU”

После успешного входа выбираем коллекцию config и изменяем нужный параметр:

mongos> use config
mongos> db.settings.save({_id: "chunksize", value: NumberLong(32)})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

Только что мы установили размер чанка рывным 32 Mb. Проверить текущее значение данной настройки можно командой:

mongos> db.settings.find({'_id':"chunksize" })
{ "_id" : "chunksize", "value" : NumberLong(32) }

Для того чтобы управлять шардами (сперва их необходимо добавить), нужно подключиться от имени пользователя со встроенной ролью clusterAdmin. Создадим сертификат для администратора кластера:

server1.cluster.com:~/mongodb/keys# openssl req -new -nodes -newkey rsa:2048 -keyout clusterAdmin.key -out aclusterAdmin.csr
Generating a 2048 bit RSA private key
................+++
.......................................+++
writing new private key to 'clusterAdmin.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: RU
State or Province Name (full name) [Some-State]: MoscowRegion
Locality Name (eg, city) []: Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]: SomeSystems
Organizational Unit Name (eg, section) []: Statistics
Common Name (e.g. server FQDN or YOUR name) []: clusteradmin
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

server1.cluster.com:~/mongodb/keys# openssl x509 -CA mongodb-CA-cert.crt -CAkey mongodb-private.key -CAcreateserial -req -days 36500 -in clusterAdmin.csr -out clusterAdmin.crt
Signature ok
subject=/C=RU/ST=MoscowRegion/L=Moscow/O=SomeSystems/OU=Statistics/CN=clusteradmin
Getting CA Private Key
Enter pass phrase for mongodb-private.key:

server1.cluster.com:~/mongodb/keys# cat clusterAdmin.key clusterAdmin.crt > clusterAdmin.pem

server1.cluster.com:~/mongodb/keys# openssl x509 -in clusterAdmin.pem -inform PEM -subject -nameopt RFC2253
subject= CN=clusteradmin,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU
-----BEGIN CERTIFICATE-----

Ничего для нас необычного, только не забываем указывать OU отличный от OU указанного для участников кластера.

Теперь снова подключимся к монгосу и пройдем аутентификацию от имени root, и добавим нового пользователя – администратора кластера:

mongos> db.getSiblingDB("$external").runCommand({
createUser: "CN=clusteradmin,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "clusterAdmin", db: "admin"}]
})

Переподключимся к mongos под администратором кластера (аутентификация включена в команду подключения):

mongo --ssl --sslCAFile /root/mongodb1/keys/mongodb-CA-cert.crt --sslPEMKeyFile /root/mongodb1/keys/clusterAdmin.pem --host server1.cluster.com --port 27017 --authenticationMechanism "MONGODB-X509" --authenticationDatabase "$external" -u “CN=clusteradmin,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU”

Добавляем шарды, которые указываем в формате реплика сетов, исключив инстансы-арбитры:

mongos> sh.addShard("rs0/server1.cluster.com:27000,server2.cluster.com:27000")
mongos> sh.addShard("rs1/server1.cluster.com:27001,server2.cluster.com:27001")
mongos> sh.addShard("rs2/server1.cluster.com:27002,server2.cluster.com:27002")

Если с добавлением шардов все прошло успешно, можем посмотреть текущий статус командой шардирования:

mongos> sh.status()

--- Sharding Status ---
 sharding version: {
   "_id" : 1,
   "minCompatibleVersion" : 5,
   "currentVersion" : 6,
   "clusterId" : ObjectId("5795284cd589624d4e36b7d4")
}
 shards:
   {  "_id" : "rs0",  "host" : "rs0/server1.cluster.com:27100,server2.cluster.com:27200" }
   {  "_id" : "rs1",  "host" : "rs1/server1.cluster.com:27101,server2.cluster.com:27201" }
   {  "_id" : "rs2",  "host" : "rs2/server1.cluster.com:27102,server2.cluster.com:27202" }
 active mongoses:
   "3.2.8" : 1
 balancer:
   Currently enabled:  yes
   Currently running:  no
   Failed balancer rounds in last 5 attempts:  0
   Migration Results for the last 24 hours:
       No recent migrations
 databases:

мы видим наши шарды, видим состояние балансировщика – он включен, но сейчас бездействует так как у него пока нет данных для миграции чанков которые он бы распределял между доступными шардами. Об этом нам говорит пустой список “databases”. Таким образом мы построили шардированный кластер, но по-умолчанию у всех коллекций всех баз данных шардирование отключено. Включается оно в два этапа:

Этап 1. Включаем шардирование для нужной базы. В нашем случае это analyitcs:

mongos> sh.enableSharding("statistics")

Проверяем результат:

mongos> sh.status()
--- Sharding Status ---
 sharding version: {
   "_id" : 1,
   "minCompatibleVersion" : 5,
   "currentVersion" : 6,
   "clusterId" : ObjectId("5795284cd589624d4e36b7d4")
}
 shards:
   {  "_id" : "rs0",  "host" : "rs0/server1.cluster.com:27000,server2.cluster.com:27000" }
   {  "_id" : "rs1",  "host" : "rs1/server1.cluster.com:27001,server2.cluster.com:27001" }
   {  "_id" : "rs2",  "host" : "rs2/server1.cluster.com:27002,server2.cluster.com:27002" }
 active mongoses:
   "3.2.8" : 1
 balancer:
   Currently enabled:  yes
   Currently running:  no
   Failed balancer rounds in last 5 attempts:  0
   Migration Results for the last 24 hours:
       No recent migrations
 databases:
   {  "_id" : "analytics",  "primary" : "rs2",  "partitioned" : true }

В списке баз данных должна появиться база analytics, мы также видим, что Primary-шардом (не путать с PRIMARY Реплика Сета) для этой базы данных был назначен шард „rs2“. Это означает, что все документы коллекций с отключенным шардированием будут целиком храниться на этом Primary-шарде (rs2).

Этап 2. Включаем шардирование для коллекции.

Как говорилось ранее для разбиения всей совокупности документов шардируемой коллекции на чанки, монге нужен ключевой индекс – ключ шардирования. Его выбор является весьма ответственной задачей, к которой нужно подходить с умом, руководствуясь требованиями вашей реализации и здравым смыслом. Индекс, по которому будет производиться разделение коллекции на чанки выбирается из существующих индексов, либо добавляется в коллекцию намерено. Так или иначе на момент включения шардирования, соответствующий ключу индекс должен существовать в коллекции. Ключ шардирования не накладывает особых ограничений на соответствующий индекс. При необходимости его можно сделать составным, например {“s”: 1, “ts”: -1}.

Определившись с индексом, который нам нужен создаем его и указываем в качестве ключа шардирования для коллекции statistics в базе данных analytics. Как я уже говорил наиболее репрезентативным полем нашей коллекции statistics, является идентификатор сенсора – поле s. Если у вас еще не создан соответствующий индекс в коллекции, то самое время его создать:

mongos> use analytics
mongos> db.statistics.ensureIndex({"s":1})

Включаем шардирование коллекции с указанием ключевого индекса шардирования:

mongos> sh.shardCollection("analytics.statistics", {"s":1})

С этого момента мы действительно можем говорить о шардировании данных в нашем кластере. После включения шардирования для коллекции, она будет разбита на чанки (количество зависит от размера данных и размера самого чанка), которые первоначально будут находиться в PRIMARY-шарде, а затем в процессе балансировки (миграции) будут поделены между другими шарадами. Процесс балансинга на мой взгляд происходит весьма неторопливо. В нашем случае коллекция из 3М записей распределялась между тремя шардами более недели.

Спустя некоторое время, давайте снова запустим команду sh.status() и посмотрим, что изменилось:

mongos> sh.status()
--- Sharding Status ---
 sharding version: {
   "_id" : 1,
   "minCompatibleVersion" : 5,
   "currentVersion" : 6,
   "clusterId" : ObjectId("5773899ee3456024f8ef4895")
}
 shards:
   {  "_id" : "rs0",  "host" : "rs0/server1.cluster.com:27000,server2.cluster.com:27000" }
   {  "_id" : "rs1",  "host" : "rs1/server1.cluster.com:27001,server2.cluster.com:27001" }
   {  "_id" : "rs2",  "host" : "rs2/server1.cluster.com:27002,server2.cluster.com:27002" }
 active mongoses:
   "3.2.8" : 1
 balancer:
   Currently enabled:  yes
   Currently running:  yes
       Balancer lock taken at Sun Jul 29 2016 10:18:32 GMT+0000 (UTC) by MongoDB:27017:1468508127:-1574651753:Balancer
   Collections with active migrations:
       statistic.statistic started at Sun Jul 29 2016 10:18:32 GMT+0000 (UTC)
   Failed balancer rounds in last 5 attempts:  0
   Migration Results for the last 24 hours:
       3 : Success
       2 : Failed with error 'aborted', from rs2 to rs0
 databases:
   {  "_id" : "analytics",  "primary" : "rs2",  "partitioned" : true }
       analytics.statistics
             shard key: { "s" : 1 }
             unique: false
             balancing: true
             chunks:
                   rs0    1
                   rs1    2
                   rs2    21
             too many chunks to print, use verbose if you want to force print

В базе analytics для которой мы ранее включили шардирование появилась коллекция statistics у которой мы видим текущий ключ шардирования shard key. Также в выводе можно обнаружить распределение чанков по шардам и если у вас небольшое количество чанков в коллекции, то вы также увидите краткую сводку по чанкам. Также в разделе balancer мы можем увидеть информацию об успешных миграциях чанков, либо об ошибках за последние сутки.

Supervisor

После установки стандартного пакета MongoDB Community в нашей системе появляется служба mongodb представляющая “коробочный” вариант сервера. Эта служба запускается по-умолчанию после установки MongoDB.

Запуск службы обеспечивает скрипт демонизации располагающийся по пути: /etc/init.d/mongod. Как вы уже могли заметить, нам необходимо запускать на одной машине несколько экземпляров mongod и по одному mongos для серверов данных server1.cluster.com и server2.cluster.com.

На первый взгляд есть готовое решение на примере скрипта /etc/init.d/mongod, но мне более удобным и прозрачным показался вариант с использованием утилиты supervisor.

Supervisor дает нам также небольшой плюс в виде возможности одновременного запуска и остановки всех наших mongo{d/s}’ов командами:

supervisorctl start all
supervisorctl stop all

(при условии если на машине нет больше иных приложений запускаемых супервизором — как в нашем случае).
Устанавливается пакет supervisor на большинстве операционных систем семейства linux из стандартного рапозитария, в моем случае (Debian 8) будет актуальна команда:

# apt-get install supervisor

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

Приведу пример конфигурации mongod для реплики rs0:

#
# /etc/supervisor/conf.d/mongod-rs0.conf
#

[program:mongod-rs0]
command=mongod --config /root/mongodb/cfg/rs0.conf
user=root 
stdout_logfile=/root/mongodb/logs/supervisor/mongod-rs0-stdout.log
redirect_stderr=true
autostart=true
autorestart=true
stopwaitsecs=60

В квадратных скобках мы определяем идентификатор приложения, который будем использовать для запуска или остановки. Параметр command собственно задает команду, которую супервизору необходимо выполнить – mongod принимающий конфигурационный файл. Далее указываем юзера от чьего имени будет запущен процесс. Параметр stdout_logfile – задает путь к файлу вывода в который будет писать supervisor. Это бывает полезным когда что-то пошло не так, и нужно понять, почему супервизор не стартует приложение.

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

Также полезным будет изменить параметр stopwaitsecs который заставит супервизор при остановке приложения ожидать указанное количество секунд. По-умолчанию при остановке приложения супервизор шлет сигнал TERM, затем ожидает 10 секунд. Если по их истечении приложение не завершилось он отправляет уже сигнал KILL, который не может быть проигнорирован приложением и теоретически может привести к потере данных. Поэтому рекомендуется увеличить дефолтный интервал ожидания завершения приложения.

Сформированный конфигурационный файл необходимо положить в соответствующий каталог супервизора, как правило в ОС linux – это /etc/supervisor/conf.d/.

Когда все будет готово нужно обновить конфигурацию супервизора командой:

# supervisorctl reload

Остановка, запуск и проверка состояния сконфигурированного приложения выполняется соответственно командами:

# supervisorctl stop mongod-rs0
# supervisorctl start mongod-rs0
# supervisorctl status mongod-rs0

После перехода на использование supervisor важно не допустить запуск стандартной службы mongodb, которая может занять порт 27017, (например после перезапуска сервера) на котором мы запускаем mongos. Для этого можно просто удалить скрипт /etc/init.d/mongod.

Полезная информация

Включение шардинга для больших коллекций

Самая загруженная коллекция нашей БД на момент миграции насчитывала чуть более 3M записей и во время тестов включение шардинга для такой коллекции (команда sh.shardCollection() ), отлично выполнялось. Однако проводились тесты и на искусственно сгенерированной БД с 100M аналогичных записей. На таком объеме команда sh.shardCollection() через некоторое время завершается с ошибкой “timeout”. Выходом из данной ситуации становится следующий порядок действий:

Шаг 1. Импортируем всю базу данных на кластер;
Шаг 2. На рабочем сервере или уже на кластере создаем дамп отдельной “большой” коллекции, например:

mongoexport --db analytics --collection statistics --out statistics.json

Шаг 3. Удаляем на кластере “большую” коллекцию:

> use analytics
> db.statistics.drop()

Шаг 4. Создаем пустую “большую” коллекцию и добавляем в нее индекс, по которому будем шардировать:

> db.analytics.ensureIndex({"s":1})

Шаг 5. Включаем шардирование коллекции с указанием ключа шардирования:

> sh.shardCollection("analytics.statistics", {"s":1})

Шаг 6. И теперь импортируем данные коллекции:

mongoimport --db analytics --collection statistics --file statistics.json

Такой прием сработал у меня, однако нужно учитывать, что экспорт/импорт большой коллекции в формате json процесс не быстрый.

Создание резервной копий базы данных

Создание резервной копии всех компонент шардированного кластера весьма сложная процедура, которая требует, чтобы балансировщик находился в выключенном состоянии (при этом его нельзя остановить принудительно во время миграции), блокировки SECONDARY-узлов на каждом шарде для последующего их резервного копирования. Подробнее о выполнении полного резервного копирования вы можете прочитать в официальной документации.

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

Резервное копирование базы analytics мы будем выполнять при помощи утилиты mongodump которая идет в составе пакета MongoDB Community.

В MongoDB есть специальная встроенная роль backup, обладающая минимальным набором прав для выполнения резервного копирования данных. Для выполнения данной процедуры мы заведем отдельного пользователя и по традиции сперва сгенерируем ему x.509 сертификат. Я не буду приводить всю процедуру генерации сертификата, она неоднократно демонстрировалась в статье, скажу лишь, что у вас должен получиться следующий subject:

CN=backuper,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU

Теперь подключимся к кластеру и создадим пользователя backuper с built-in ролью backup:

mongos> db.getSiblingDB("$external").runCommand({
createUser: "CN=backuper,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
roles: [{role: "backup", db: "admin"}]
})

После создания пользователя можно попробовать выполнить резервное копирование нашей БД analytics. Аргументы команды для утилиты mongodump аналогичны подключению с аутентификацией, только дополнительно указывается название БД (--db), каталог куда сохранится дамп (-o), а также аргумент --gzip указывающий, что нужно сжимать все файлы дампа:

mongodump --ssl --sslCAFile “/root/mongodb/keys/mongodb-CA-cert.crt” --sslPEMKeyFile “/root/mongodb/keys/backuper.pem” -u "CN=backuper,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU" --host server1.cluster.com --port 27017 --authenticationMechanism "MONGODB-X509" --authenticationDatabase "$external" --db analytics --gzip -o "/path/to/backup/"

Немного программного кода...

В конце статьи я хочу поделиться примерами программного кода, где продемонстрирую подключение к созданному кластеру. Так как наш сервис работающий с кластером состоит из множества частей написанных на С++ и Python, примеры будут именно на этих замечательных языках программирования.

Итак, начнем с примера на C++. Предложенный ниже пример подключения актуален для официального драйвера MongoDB mongodb-cxx-driver-legacy-1.1.1.

#include <mongo/client/dbclient.h>
#include <mongo/client/options.h>

...

mongo::DBClientConnection client(true); // включаем автореконнект
try {
      // заполняем структуру опций SSL подключения
      mongo::client::Options options;
      options.setSSLMode(mongo::client::Options::SSLModes::kSSLRequired);
      options.setSSLCAFile("/path_to_certs/mongodb-CA-cert.crt");
      options.setSSLPEMKeyFile("/path_to_certs/analyticsuser.PEM");

      mongo::Status status = mongo::client::initialize(options);
      mongo::massertStatusOK(status); // проверим, все ли в порядке

      client.connect("www.server1.cluster.com:27017"); // адрес и порт хоста на котором запущен mongos

      // настройки аутентификации: бд, пользователь, механизм
      mongo::BSONObjBuilder auth_params;
      auth_params.append("db", "$external");
      auth_params.append("user", "CN=username,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU");
      auth_params.append("mechanism", "MONGODB-X509");

      client.auth(auth_params.obj()); // выполняем аутентификацию
} catch (const mongo::DBException &e) {
      std::cout << "DBException : " << e.toString() << std::endl;
}

...

Перед тем как выполнить подключение к хосту базы данных, нам необходимо проинициализировать клиента, структурой mongo::client::Options, указав уровень требования SSL (kSSLRequired), публичный сертификат CA (mongodb-CA-cert.crt), и PEM-файл привязанный к пользователю кластера (в данном случае — это analyticsuser, которого мы создали ранее).

Далее мы подключаемся к БД и если все проходит успешно выполняем аутентификацию. Обратите внимание на имя базы данных через которую проходит аутентификация – “$external”, в качестве имени передаем subject из сертификата пользователя, не забываем указать механизм аутентификации. Также видим, что пароль мы не передаем т.к. наша аутентификация, является внешней – через проверку подлинности сертификата.

В веб-части проекта, написанной на Python задействован драйвер pymongo чистом виде, а объектная модель сформирована с использованием фреймворка mongoengine.

Для начала пример для pymongo:

import ssl

db_hosts="server1.cluster.com:27017,server2.cluster.com:27017"
db_port=None
     
client = MongoClient(db_hosts,
             db_port,
             read_preference=ReadPreference.NEAREST,
             ssl=True,
             ssl_certfile="/path_to_certs/analyticsuser.PEM",
             ssl_cert_reqs=ssl.CERT_REQUIRED,
             ssl_ca_certs="/path_to_certs/mongodb-CA-cert.crt")
db = client[db_name]
db.authenticate(name=db_user, source="$external", mechanism="MONGODB-X509")

Ни чего особенного — мы также передаем публичный CA сертификат и клиентский PEM-файл. Внимания здесь заслуживает переменная db_hosts – это фактически строка подключения в которой через запятую перечислены адреса и порты на которых доступны монгосы. Параметр порт (db_port), можно в нашем случае не указывать, я его привел для явности. Драйвер pymongo, подключенный таким образом в случае недоступности первого адреса, автоматически предпримет попытку переподключения ко второму адресу и наоборот. Практика показывает, что в случае доступности обоих серверов при первом подключении адреса выбираются по порядку, т.е. первым будет подключение к server1.cluster.com:27017.

Однако при тестировании данного поведения pymogo, было замечено, что автоматическому переподключению предшествует порождение исключения pytmogo.errors.AutoReconnect. Для обработки данной ситуации был написан небольшой декоратор, который позволяет обернуть, например, функции отображения страницы статистики или API-запроса на чтение данных:

from functools import wraps
from pymongo.errors import AutoReconnect
import time

def pymongo_reconnect(attempts=5):
  def decorator(f):
      @wraps(f)
      def decorated_function(*args, **kwargs):
         

          tries_reconnect = attempts
          if tries_reconnect <= 0:
              tries_reconnect = 1

          while tries_reconnect:
              try:
                  return f(*args, **kwargs)
              except AutoReconnect as ar:
                  tries_reconnect -= 1
                  print("Caught AutoReconnect exception.")
                  if tries_reconnect <= 0:
                      raise ar
                  time.sleep(0.1)
                  print("Attempt to reconnect (%d more)...n" % tries_reconnect)
                  continue

      return decorated_function
  return decorator

Декоратор дает некоторое количество попыток выполнить функцию (в данном случае 5) и истратив все попытки завершается исключением.

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

PRIMARY — всегда читать данные с primary-участника реплики шарда; PRIMARY_PREFERRED — читать из primary-участника реплики шарда, но если это невозможно читать из secondary;
SECONDARY — читать только из secondary-учаcтника шарда;
SECONDARY_PREFERRED — читать по возможности из secondary шарда, но если невозможно из primary;
NEAREST — читать из любого доступного (так сказано в документации pymongo), причем документации самой монги подробно расписано, что используется не просто первый попавшийся участник реплики, а тот у которого наименьшая сетевая задержка — попросту пинг, не взирая кто предоставит данные primary или secondary.

Таким образом данный параметр с одной стороны дает нам возможность разгрузить PRIMARY-инстансы от нагрузки запросов на чтнеие, но с другой стороны может привести к получению неактуальных/неконсистентных данных, т.к. у SECONDARY-инстансов так или иначе есть задержка на синхронизацию с PRIMARY (зависист от конфигруации вашей реплики и лага). Поэтому выбирать данный вариант стоит с осторожностью и исходя из допущений и ограничений вашей системы.

Следует также заметить, что в случае невозможности выполнить предпочтения PRIMARY или SECONDARY pymongo будет генерировать исключение OperationFailure, поэтому необходимо учитывать такое поведение при использовании данных вариантов.

С пакетом mongoengine все оказалось более печально. Первым делом я увидел в проекте точку подключения к БД посредством пакета mongoengine:

connect('default', host, port)

OK, подумал я: “сейчас я передам в mongoengine.connect остальные параметры подключения как было с pymongo и на этом дело решено”. Но мои чаяния были напрасными так как у mongoengine.connect я не обнаружил нужных мне параметров — она является всего лишь общей оберткой для функции с более широким списком аргументов: mongoengine.register_connection. Среди параметров данной функции также не оказалось необходимого через который можно было бы передать в подключение механизм авторизации MONGODB-X509. Я сделал пару тщетных попыток в надежде, что фреймворк „поймет“ что от него требуется, но покапавшись в исходниках, убедился в отсутствии даже не поддержки, а отсутствии возможности “пробросить” нужный механизм в mogoengine где его поймет pymongo (на которой собственно и основан mongoengine).

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

Таким образом подключение с x.509 аутентификацией приняло следующий вид:

import ssl
from mongoengine import DEFAULT_CONNECTION_NAME, register_connection


db_hosts="server1.cluster.com:27017,server2.cluster.com:27017"
db_port=None

ssl_config = {
    'ssl': True,
    'ssl_certfile': "/path_to_certs/analyticsuser.PEM",
    'ssl_cert_reqs': ssl.CERT_REQUIRED,
    'ssl_ca_certs': "/path_to_certs/mongodb-CA-cert.crt",
  }

register_connection(alias=DEFAULT_CONNECTION_NAME,
                  name="statistic",
                  host=db_hosts,
                  port=db_port,
                  username="CN=username,OU=StatisticsClient,O=SomeSystems,L=Moscow,ST=MoscowRegion,C=RU",
                  password=None,
                  read_preference=ReadPreference.NEAREST,
                  authentication_source="$external",
                  authentication_mechanism="MONGODB-X509",
                  **ssl_config)

К сожалению пока мне не удалось добиться слияния с основным репозитаем MongoEngine, т.к. не проходят тесты на всех комбинациях python/pymongo. В последних пул-реквестах многих разработчиков я заметил схожие проблемы с тем же тестами, поэтому закрадывается мысль о возможной неполадке в “стабильной” ветке фреймворка.

Надеюсь в ближайшее время ситуация наладится, удастся разобраться в проблеме, и поддержка аутентификации по x.509 появится в основном репозитарии MongoEngine.

Автор: zeezdev

Источник

Поделиться новостью