Налаштування MongoDB ShardedCluster з X509 аутентифікацією

Всім доброго часу доби! Нещодавно життя підкинула автору захоплюючу роботу з розгортання MongoDB кластера з налаштуванням реплікації і шардирования, а також використанням 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 (можливо хтось підкаже хороше рішення в коментарях, буду вдячний!) для видачі потрібного сервера за георасположению клієнта.

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



Первинна настройка
Зайдемо на 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.001 Z'), '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.483 Z'), '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.001 Z'), '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 в 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 blank some
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 []:

Підписуємо сертифікат (знову знадобиться секретна фраза від СА ключа):

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=UA/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, тепер уже з відповідним конфігураційним файлом. Те ж саме потрібно зробити на другому і третьому серверах. Таким чином у нас повинні бути запущені всі монгоды першої репліки (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 blank some
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=UA/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>» бачимо до кого ми підключилися, значить все ОК.

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 blank some
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=UA/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 blank some
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=UA/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 --statistics collection --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 --statistics collection --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=UK

Тепер підключимося до кластеру і створимо користувача 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-учасника шарда;
SECONDARY_PREFERRED — читати по можливості з secondary шарда, але якщо неможливо primary;
NEAREST — читати з будь-якого доступного (так сказано в документації pymongo), причому документації самій монги детально розписано, що використовується не просто перший-ліпший учасник репліки, а той у якого найменша мережна затримка — просто пінг, не дивлячись хто надасть дані primary або secondary.

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

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

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

connect('default', host, port)

ДОБРЕ, подумав я: “зараз я передам в 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.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.