The words you are searching are inside this book. To get more targeted content, please make full-text search by clicking here.

Базовая книга по построению микросервисной архитектуры с практическими примерами на Node.js. Также рассмотрена работа с оркестратором Kubernetes и контейнерами Docker в среде Docker Compose. Разобраны основные принципы и техники разработки распределенных систем, в частности показано, как написать и развернуть три микросервиса, управлять СУБД, настроить брокер сообщений Kafka, внедрить кэш Redis. Объяснены паттерны проектирования. Особое внимание уделено распределенным транзакциям и разворачиванию микросервисов на удаленном сервере. Показано, как обеспечить расширяемость и отказоустойчивость приложений, как успешно бороться с усложнением клиентских интерфейсов, поддерживая высокую скорость загрузки страниц и приложений и не забывая о том, чтобы сайт оставался красивым и удобным в использовании. Электронный архив на сайте издательства содержит дополнительные материалы к книге.

Discover the best professional documents and content resources in AnyFlip Document Base.
Search
Published by BHV.RU Publishing House, 2024-02-14 00:40:10

Node.js: разработка приложений в микросервисной архитектуре с нуля

Базовая книга по построению микросервисной архитектуры с практическими примерами на Node.js. Также рассмотрена работа с оркестратором Kubernetes и контейнерами Docker в среде Docker Compose. Разобраны основные принципы и техники разработки распределенных систем, в частности показано, как написать и развернуть три микросервиса, управлять СУБД, настроить брокер сообщений Kafka, внедрить кэш Redis. Объяснены паттерны проектирования. Особое внимание уделено распределенным транзакциям и разворачиванию микросервисов на удаленном сервере. Показано, как обеспечить расширяемость и отказоустойчивость приложений, как успешно бороться с усложнением клиентских интерфейсов, поддерживая высокую скорость загрузки страниц и приложений и не забывая о том, чтобы сайт оставался красивым и удобным в использовании. Электронный архив на сайте издательства содержит дополнительные материалы к книге.

Keywords: Node.js, NestJS, PostgreSQL, Kafka, Redis, Docker, Docker-Compose, Kubernetes

Юлия Попова Санкт-Петербург «БХВ-Петербург» 2024


УДК 004.414.2 ББК 32.973.26-018.1 П58 Попова Ю. Ю. П58 Node.js: разработка приложений в микросервисной архитектуре с нуля. — СПб.: БХВ-Петербург, 2024. — 256 с.: ил. — (С нуля) ISBN 978-5-9775-1935-9 Базовая книга по построению микросервисной архитектуры с практическими примерами на Node.js. Также рассмотрена работа с оркестратором Kubernetes и контейнерами Docker в среде Docker Compose. Разобраны основные принципы и техники разработки распределенных систем, в частности показано, как написать и развернуть три микросервиса, управлять СУБД, настроить брокер сообщений Kafka, внедрить кеш Redis. Объяснены паттерны проектирования. Особое внимание уделено распределенным транзакциям и разворачиванию микросервисов на удаленном сервере. Показано, как обеспечить расширяемость и отказоустойчивость приложений, как успешно бороться с усложнением клиентских интерфейсов, поддерживая высокую скорость загрузки страниц и приложений и не забывая о том, чтобы сайт оставался красивым и удобным в использовании. Электронный архив на сайте издательства содержит дополнительные материалы к книге. Для начинающих веб-разработчиков УДК 004.414.2 ББК 32.973.26-018.1 Группа подготовки издания: Руководитель проекта Олег Сивченко Зав. редакцией Людмила Гауль Редактор Григорий Добин Компьютерная верстка Ольги Сергиенко Дизайн обложки Зои Канторович "БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул., 20 ISBN 978-5-9775-1935-9 © ООО "БХВ", 2024 © Оформление. ООО "БХВ-Петербург", 2024


Оглавление Введение ............................................................................................................................ 7 Для кого эта книга? ......................................................................................................................... 7 Обзор Node.js ................................................................................................................................... 7 Выбор фреймворка: NestJS или Express.js? ................................................................................. 10 Сравнение монолитной и микросервисной архитектуры ........................................................... 13 Список литературы и источников ................................................................................................ 14 Глава 1. Разработка первого микросервиса (User) ................................................. 15 Настройка локального окружения ................................................................................................ 15 Редактор кода и плагины ....................................................................................................... 15 Node.js и пакетные менеджеры ............................................................................................. 16 NVM ........................................................................................................................................ 18 Установка NestJS ................................................................................................................... 19 Git ............................................................................................................................................ 21 Codestyle ................................................................................................................................. 23 Docker и Docker-Compose ..................................................................................................... 24 Тестирование API .................................................................................................................. 27 Вывод ...................................................................................................................................... 29 Создание структуры проекта ........................................................................................................ 29 Подключение необходимых библиотек ....................................................................................... 37 Переменные окружения ........................................................................................................ 37 ORM ........................................................................................................................................ 39 Swagger ................................................................................................................................... 45 Библиотеки сlass-transformer и class-validator .................................................................... 47 Настройка ESLint ........................................................................................................................... 48 Проектирование базы данных PostgreSQL .................................................................................. 52 Что такое «база данных» и какие они бывают? ................................................................... 52 Нормализация данных ........................................................................................................... 54 Первая нормальная форма (1НФ) ................................................................................ 54 Вторая нормальная форма (2НФ) ................................................................................ 55 Третья нормальная форма (3НФ) ................................................................................ 56 Разработка бизнес-логики и маршрутизации для модуля User .................................................. 57 Тестирование микросервиса ......................................................................................................... 74 Список литературы и источников ................................................................................................ 77


4 Оглавление Глава 2. Разработка микросервиса авторизации и аутентификации (Auth) ....... 79 Теоретический обзор способов авторизации и аутентификации ............................................... 79 Аутентификация, идентификация и авторизация ................................................................ 79 Аутентификация по паролю .................................................................................................. 80 Аутентификация по сертификатам ....................................................................................... 81 Аутентификация по одноразовым паролям ......................................................................... 82 Аутентификация по ключам доступа ................................................................................... 83 Аутентификация по токенам ................................................................................................. 83 Базовые меры предосторожности от возможных уязвимостей ................................................. 85 Переполнение буфера ............................................................................................................ 86 Состояние гонки .................................................................................................................... 86 Атаки проверки ввода ............................................................................................................ 86 Атаки аутентификации .......................................................................................................... 87 Атаки авторизации ................................................................................................................. 87 Атаки на стороне клиента ..................................................................................................... 88 Разработка модуля Auth ................................................................................................................ 88 Список литературы и источников .............................................................................................. 101 Глава 3. Способы взаимодействия между микросервисами .............................. 103 HTTP-протокол ............................................................................................................................ 103 Модель OSI ........................................................................................................................... 103 Физический уровень ................................................................................................... 104 Канальный уровень ..................................................................................................... 104 Сетевой уровень .......................................................................................................... 105 Транспортный уровень ............................................................................................... 105 Сеансовый уровень ..................................................................................................... 105 Уровень представления .............................................................................................. 106 Прикладной уровень ................................................................................................... 106 Устройство HTTP-протокола .............................................................................................. 106 Структура HTTP-запроса ........................................................................................... 107 Cтруктура HTTP-ответа ............................................................................................. 108 gRPC ............................................................................................................................................. 108 RabbitMQ ...................................................................................................................................... 112 Apache Kafka ................................................................................................................................ 115 Redis .............................................................................................................................................. 117 Разработка интеграции сервисов Auth и Account...................................................................... 118 Список литературы и источников .............................................................................................. 136 Глава 4. Разработка модуля Transaction ................................................................ 137 Проектирование базы данных .................................................................................................... 137 Нормальная форма Бойса — Кодда (НФБК) — частная форма третьей нормальной формы ................................................................................................ 137 Четвертая нормальная форма ............................................................................................. 139 Пятая нормальная форма .................................................................................................... 139 Доменно-ключевая нормальная форма .............................................................................. 140 Шестая нормальная форма .................................................................................................. 140 Понятия миграций и транзакций в контексте базы данных PostgreSQL ................................. 141 Миграции .............................................................................................................................. 141 Индексы ................................................................................................................................ 144


Оглавление 5 Транзакции ........................................................................................................................... 150 ACID ............................................................................................................................ 151 Параллельные транзакции .......................................................................................... 151 Уровни изоляции транзакций в SQL ......................................................................... 152 Разработка модуля Transaction ................................................................................................... 153 Интеграция Transaction и Account .............................................................................................. 167 Проблема распределенных транзакций ..................................................................................... 183 Двухфазная фиксация .......................................................................................................... 184 Saga ....................................................................................................................................... 185 Реализация паттерна Saga .......................................................................................... 186 Список литературы и источников .............................................................................................. 209 Глава 5. Развертывание микросервисов ................................................................ 211 Обертывание микросервисов в docker-контейнеры .................................................................. 225 Масштабирование при помощи оркестратора Kubernetes ....................................................... 238 Заключение ................................................................................................................................... 247 Список литературы и источников .............................................................................................. 247 Приложение. Описание электронного архива ....................................................... 249 Предметный указатель .............................................................................................. 251


Введение Цель этой книги — показать на практике, как разрабатываются микросерверные приложения на Node.js. Здесь всё начинается с одного небольшого микросервиса, выполняющего определенный набор задач, а заканчивается уже группой из микросервисов, применением множества технологий и доставкой готового приложения на виртуальные серверы. Для кого эта книга? Основное внимание в ней уделяется именно практическим навыкам написания микросервисов. Она пригодится тем, кто достаточно глубоко владеет знаниями о JavaScript и TypeScript. Упор делается именно на конкретный стек технологий: Node.js, NestJS, PostgreSQL, Kafka, Redis, Docker, Docker-Compose, Kubernetes и некоторые другие. Желательно, чтобы у вас был опыт написания приложений на NestJS, т. к. глубоко в сами технологии мы погружаться не станем. Главный акцент сделан на микросервисах, проблемах, с которыми вы можете столкнуться при их разработке, и путях их решения. Обзор Node.js Node.js — это в первую очередь платформа, позволяющая применять язык JavaScript для бэкенда. Веб-разработку в наши дни невозможно представить без JavaScript. Это основной язык и инструмент фронтенд-разработчика. С него начинается TypeScript и всевозможные фреймворки, ускоряющие разработку, — например, такие как ReactJS, Angular, Vue. Однако фронтенд не может жить без бэкенда, и долгое время в роли бэкенда для веб-разработки использовались в основном PHP и Java. Сейчас же все чаще для разработки выбирают именно Node.js, и связано это как с меньшими затратами на разработку — фронтенд-разработчику не нужно учить дополнительный язык и его тонкости, достаточно посмотреть фреймворки конкретно под бэкенд-разработку, так и с тем, что асинхронность, которую предоставляет эта платформа, очень хорошо подходит для веб-нагрузки. К важным преиму-


8 Введение ществам Node.js также относят событийно-ориентированное и асинхронное программирование. Node.js стоит, как говорится, на двух китах: библиотеке libuv и движке V8 от Google, применяемом в браузере. Libuv — это кросс-платформенная низкоуровневая библиотека, написанная на Си и ориентированная на асинхронный ввод/вывод. Асинхронность — вообще основополагающая вещь в контексте разработки на Node.js, поэтому остановимся на ней подробнее. Асинхронностью называют подход в программировании, при котором несколько задач может выполняться одновременно, не блокируя основной поток выполнения. При синхронном (последовательном) подходе программа выполняется по порядку, и, если, к примеру, мы запустим бесконечный цикл, то это заблокирует всю программу. В случае же, если мы запустим бесконечный цикл асинхронно, то приложение продолжит полноценно работать. Это происходит благодаря тому, что асинхронный код выполняется, не блокируя основной поток выполнения, а в случае с Node.js используется так называемый event loop (цикл событий), который и позволяет обрабатывать несколько задач одновременно. Но стоит учитывать, что асинхронность не является «серебряной пулей», т. к. если выполнение цикла занимает много времени и ресурсов, это все равно скажется на производительности и доступности приложения. Как следствие, приложение может сильно замедлиться, хотя и будет продолжать работать. Event loop — это механизм в Node.js для работы с внешними ресурсами (файловой системой, базами данных, сетевыми запросами и т. п.) и асинхронным кодом. Подробнее останавливаться на его устройстве мы не будем, т. к. это отдельная и большая тема. Основное, что стоит знать об event loop, — это то, что благодаря ему у нас есть возможность работать с неблокирующим вводом/выводом. Отдельно отмечу, что браузерный event loop и серверный имеют небольшие, но важные различия. Собственно, libuv в Node.js как раз и предоставляет возможность выполнения асинхронных операций ввода/вывода, таймеров и сетевых операций. Вторым важным компонентом в Node.js является движок V8. Он-то и обеспечивает выполнение JavaScript. Изначально этот движок использовался только в браузере Google Chrome, но позже появились и другие браузеры на его основе, и также Node.js, поскольку этот JS-движок на самом деле независим от браузера. Отдельного внимания заслуживает встроенный пакетный менеджер npm, который буквально формирует Node.js как собственную экосистему. Вы можете использовать уже существующие библиотеки, написанные другими разработчиками, либо написать собственную и опубликовать ее, чтобы они также могли ее использовать. Вокруг этого движения сформировалось огромное сообщество — сторонние разработчики помогают в развитии продуктов, которыми пользуются, отправляя в проект, где обнаружили какой-либо баг, issuer или пожелание. Вы также можете сами поучаствовать в разработке функциональности нужных вам пакетов или исправить там баг, затем отправив Pull Request с запросом на изменение кода.


Введение 9 Пакетный менеджер Node.js, помимо плюсов, имеет и свои минусы. Из-за большого обилия существующих библиотек необходимо каждый раз продумывать, какую лучше использовать в конкретном случае. Среди них также присутствуют и «мусорные» библиотеки, которые просто есть, хотя ими мало кто пользуется. Это такая некая «барахолка», где есть точки с товаром, на который всегда есть спрос, и есть те, на которые регулярно то приходят, то уходят. Говоря про пакетный менеджер, особо нужно отметить систему зависимостей в проекте, за которую отвечают файлы package.json и package_lock.json. Когда вы добавляете пакет к себе в сервис, это все фиксируется в соответствующих файлах. Так, в package.json перечислены зависимости и их версии, а в package_lock.json — еще и зафиксированные версии зависимостей для зависимостей. Это необходимо для того, чтобы не возникало конфликтов при переносе кода с одного компьютера на другой, — ведь некоторые изменения в библиотеках могут сломать ваш код. Однако, как и у любого инструмента, минусы у Node.js также имеются. В первую очередь стоит отметить, что если вам нужно разработать систему, затрагивающую большие вычислительные операции, то для этого однозначно следует выбрать чтото другое, т. к. тяжелый вычислительный процесс значительно замедляет работу приложения. Связано это с однопоточностью Node.js и в целом с архитектурой платформы. Обратная сторона асинхронности связана с тем, что она идеально подходит для написания веб-серверов или непосредственно операций ввода/вывода, но только не для вычислений. И если вам предстоит выполнять математические операции с большими числами, то Node.js — тоже не ваш выбор. Есть множество примеров, когда число выходит за определенные пределы точности, и JavaScript начинает считать абсолютно неверно, что может быть критично. Для Node.js также весьма важно соблюдать принципы чистой архитектуры — излишняя связанность в какой-то момент может, что называется, очень быстро «выстрелить в ногу». Событийно-ориентированная среда требует хорошо прописанных отношений между ними. Поднимая вопрос про область применения Node.js, также стоит упомянуть, что иногда он находит свое место в качестве BFF (backend for frontend) — паттерна разработки, используемого в микросервисах. Он необходим, чтобы внешние запросы приходили в один сервис, который содержит в себе уже все необходимые интеграции и знает, в каком сервисе какую информацию можно получить. Очень распространенная практика, когда фронтенд-разработчики пишут его для себя самостоятельно. Таким образом, Node.js сейчас всё чаще, если главная задача — это обработка внушительного количества запросов, применяется как полноценный бэкенд, и в этом вопросе из-за своего устройства он оказывается значительно быстрее аналогов. А большое обилие фреймворков может значительно ускорить разработку.


10 Введение Выбор фреймворка: NestJS или Express.js? Express долгое время оставался самым популярным и используемым фреймворком на Node.js. Главная задача, которую решает Express, — упрощение работы с маршрутами и в целом со всем, что связано с обработкой запросов и ошибок. Появился он в 2010 году, когда TypeScript еще не существовал, а стандартом языка считался ES5. Для контекста правильным будет сказать все же пару слов и о ES5. Сейчас код, соответствующий тому времени, уже очень редко встретишь, — разве что в совсем не обновляющихся проектах. JavaScript, каким мы знаем его сейчас, начинается с ES6, или ES2015, — до этого момента писать поддерживаемый код было, мягко говоря, сложно. Если вы углублялись в тему «async/await» и промисов, вы наверняка слышали о такой проблеме, как «hell callback» — ад обратных вызовов. То есть, чтобы работать с асинхронностью, приходилось писать ужасные функции обратного вызова. В тот момент представить на JavaScript какой-либо бэкенд было трудно, а про большие проекты с долгой поддержкой и говорить не стоило. И это была не столько проблема языка, сколько проблема синтаксиса. Так вот ES5 был выпущен в конце 2009 года, чуть позже, в 2011 году, был представлен ES5.1, и в этот промежуток времени как раз и появилась первая версия Express — в 2010 году [1]. Этот фреймворк на тот момент как раз и был заточен на все нюансы того времени, и хотя там также очень многое писалось через использование функции обратного вызова, он был при этом очень легким и простым. Как бы там ни было, большинство приложений, написанных на Express, имеют очень старую архитектуру, и разобраться в этом разработчику, который никогда не работал с ES5, сейчас весьма трудно. Выбор архитектуры кода Express оставляет на совести разработчиков — он очень гибок и не подразумевает каких-либо строгостей. Это одновременно и плюс, и минус: то, что вы разобрались в одной кодовой базе, в основе которой лежит Express, совершенно не гарантирует, что с такой же легкостью вы поймете, что делается в другом проекте с этим же фреймворком. Решения могут быть как красивыми и изящными, так и такими, что лучше бы подобное больше никогда не видеть. Вы можете столкнуться как с грамотным разделением на слои вроде привычных контроллеров и сервисов, так и с их полным отсутствием, где всё выполняется, грубо говоря, в одном файле. Еще один момент, который добавит вам проблем, если вы просчитались с архитектурой, — импорты. Если не продумать устройство проекта, чтобы каждую сущность сделать отдельной, и все в целом было бы организовано согласно принципам объектно-ориентированного программирования (ООП), может получиться большая мешанина с импортами. Они не слишком нативно понятно реализованы через require, а изменив что-то в одном месте, с легкостью можно поломать работу методов, которые эту функциональность переиспользуют. Давайте взглянем на типичный код, написанный на Express: const express = require(‘express’) const app = express();


Введение 11 const Joi = require(‘joi’); const service = require(‘../service’) app.user(express.json()); const schema = { id: Joi.number().required() } app.get(‘/api/v1/orders’, (req, res) => { const validate = Joi.validate(req.params, schema); if (validate.error){ res.status(400).send(‘validate error: ${validate.error.toString()}’); return; } result = service.getOrders(req, res) return result; } Проблема такого кода, что тут всё в одном месте: и валидация, и вызов функций... Когда проект разрастается, становится очень сложно его поддерживать. Отдельно стоит отметить в Express мидлвары (middleware). Это такие промежуточные обработчики, в которые передаются объекты req (то, что поступило в запросе), res (то, что мы должны вернуть) и next (передать управление дальше). Сначала это было действительно удобно и круто, однако со временем стало понятно, что мидлвары имеют ряд минусов. Прежде всего, это неконтролируемые мутации входящего запроса: каждая мидлвара начинает добавлять что-то свое, будь то проверка на права или что-либо другое, и когда выполнение дойдет до основной бизнеслогики, у нас оказывается, что входящий запрос уже сильно «оброс» происходящим вокруг. Плюс ко всему, у мидлвар как таковых нет контрактов — они всегда принимают res, req и next. В связи с этим другой фреймворк — Fastify — принял решение отказаться от мидлвар и использовать вместо этого хуки. Не менее важной проблемой является и то, что крайне редко применяется сочетание Express и TypeScript. Связано это с тем, что Express все же гораздо лучше поддерживает JavaScript, чем TypeScript. Как минимум он разрабатывался в то время, когда о TypeScript не было слышно вообще ничего. При этом все отмеченные проблемы не помешали Express стать в свое время самым популярным фреймворком и быть взятым за основу другими фреймворками. А что же там с NestJS? Изначально свои идеи он берет от другого js/ts фреймворка, только для фронтенд-разработчиков, — AngularJS. В первую очередь, когда говорят об NestJS как об Angular для backend, имеют в виду модульность, которая как раз и обеспечивает инкапсуляцию и инверсию зависимостей. При этом если Express — это чисто HTTP-фреймворк, то NestJS — нечто гораздо большее. Он позволяет работать не только с REST, но и c GRPS, TCP и, в принципе, с любым видом транспорта по сети. NestJS «из коробки» предоставляет набор методов для работы с множеством инструментов: от чтения переменных окружения через нативный для NestJS ConfigModule, который не нужно дополнительно настраивать, если вы не работаете с чем-то специфичным, до интеграций с различными ORM и


12 Введение брокерами сообщений. И на всё это есть документация, поясняющая, как этим пользоваться. В ней приведены довольно простые и банальные примеры, но для первого погружения вполне достаточные. Так как NestJS — это, прежде всего, модульное построение приложения, такой подход накладывает ограничения на организацию кода: вы уже не можете по своему усмотрению разбивать его, как вам хочется. Есть четкий стандарт, как код должен выглядеть: иметь модуль для контроллера, модуль для сервиса и собственно модуль, который и подключается при необходимости. Модуль позволяет создавать контекст выполнения, и через него разрешаются все зависимости. Вы не сможете просто импортировать сервис из другой сущности, не подключив при этом сам модуль. Отдельный момент — декораторы. Они облегчают написание кода и уменьшают его количество за счет возможности переиспользования и упрощения, хотя при этом в то, как они выглядят в скомпилированном виде в JavaScript, лучше не вникать. Через декоратор задается и проверка прав доступа, и обозначаются методы GET, POST или любые другие, которые актуальны для вызова конкретного контроллера. NestJS, хотя и содержит в своем названии аббревиатуру от JavaScript, на самом же деле редко можно увидеть, чтобы его там использовали, — практически все проекты, что мне довелось видеть, были написаны на TypeScript. NestJS полностью поддерживает этот язык, и сам тоже написан на нем. Это позволяет нам точно знать, какие входные данные мы ожидаем, что мы должны вернуть, и это значительно уменьшает вероятность ошибки на каждом этапе. И пока проект небольшой и не затрагивает какие-то ограничения фреймворка, все будет хорошо. Однако разобраться с NestJS может оказаться значительно сложнее, когда понадобится несколько подключений, т. е. использование динамических модулей [2]. В целом зависимость между модулями может тоже «выстреливать в ногу», поскольку есть проблема циклической зависимости. Так что многие вещи, как мы позже убедимся, заставляют делать собственные решения, а не использовать готовые от NestJS. Например, модуль конфигурации сервиса загружается вместе с приложением, а если мы хотим настроить автогенерацию миграций для нашей базы данных на основе схемы, то нужно отдельно из переменных окружения доставать значения через process.env. Тогда становится не очень понятно, для чего нужен ConfigModule. В целом все эти встроенные решения «из коробки» — просто обертки над уже существующими решениями. И, как следствие, в случае обновления версии в изначальном пакете придется подождать некоторое время, пока не обновят версию и в библиотеке-обертке. Все это приводит нас к выбору: простота, легкость и гибкость, но с отсутствием четко понятного структурированного кода, без типизации и с написанием большого количества кода собственноручно, либо же использование готовых решений от NestJS и его понятных инструкций о том, как должен быть структурирован код. Для больших проектов, от которых ожидается долгое время жизни, чаще всего выбор делают все же в пользу NestJS, чем Express, т. к. поддержка проекта всегда в первую очередь упирается в понятность и читабельность кода, и это главный


Введение 13 приоритет. Если же нужно написать маленькое приложение, которое вряд ли будет развиваться далее, — Express хороший и быстрый инструмент для этого. Сравнение монолитной и микросервисной архитектуры Изначально, когда Всемирная паутина еще лишь начинала развиваться, создавали только монолиты. В основном это было связано с тем, что создание монолитов — наиболее простой способ, а задачи и разработка тогда были менее сложными по своей функциональности. Однако с развитием технологий и расширением спектра задач разработчики начали сталкиваться с ограничениями такого подхода. Главные минусы монолитов заключались в том, что их код становилось сложно поддерживать: когда у вас команда разработчиков из 20 человек, каждое изменение в проекте будет приводить к конфликтам версий, — большому количеству людей очень тяжело работать над одной кодовой базой. Обновлять версии инструментов и внедрять новые технологии также становилось проблематичным — если вам необходимо перевести сразу весь огромный проект на другую версию, вы неизбежно будете при этом сталкиваться с конфликтами, а новые API библиотек могут ломать работающую функциональность. Если же не стараться поддерживать актуальность, будет очень сложно найти разработчиков, — мало кто хочет работать с легаси (устаревшим) кодом, поскольку это не дает развиваться специалистам [3]. Второй момент — безопасность: новые версии библиотек часто содержат важные изменения и исправления багов. То есть чем быстрее устаревает ваш код, тем больше вероятность нахождения в нем уязвимостей. Из-за стремительно разрастающейся кодовой базы сложность понимания того, что в проекте происходит, неуклонно растет. Вновь пришедшим в проект специалистам становится невероятно сложно погрузиться в него и разобраться что к чему. Соответственно разработка станет сильно замедляться: начиная от проблем с IDE (редактором кода), которому будет тяжело работать с большим проектом, и заканчивая сборками приложений, на скорость которых также влияет кодовая база. Доставка приложения до конечного потребителя тоже начнет вызывать проблемы, т. к. будет происходить в лучшем случае один раз в две недели, а то и в месяц, хотя в реальности мир слишком ускорился, чтобы считать это хорошим темпом. Сейчас все больше набирает популярность подход SaaS (Software-as-a-Service), т. е. непрерывное развертывание. Это позволяет доставлять изменения в продакшен (производственную сферу) несколько раз в сутки и желательно в рабочее время. При этом правильно настроенный цикл позволяет делать это максимально незаметно для пользователей. Плюс к сказанному, монолит может порождать больше багов при тестировании релиза. И это погружает нас в цикл: релиз → тестирование → исправление багов → тестирование. И так пока не выяснится, что критичных багов больше нет и можно «раскатывать» приложение на пользователей. Отдельным пунктом при работе с монолитом выделяют надежность. Потому что если у вас отказал монолит — у вас отказала вся система целиком. Нельзя просто


14 Введение взять и отключить часть функциональности до восстановления, что позволительно в микросервисах. Вся эта аргументация выглядит так, будто микросервисы как раз и решают отмеченные проблемы, и что смотреть в сторону монолитов не имеет смысла. Но и такой подход не вполне верен, — у микросервисов тоже есть ряд существенных недостатков. Первая задача, которая встает перед разработчиками микросервисов, — правильно разбить приложение на модули. Если же где-то на этом этапе просчитаться, то получится нечто, взявшее недостатки и от монолитов, и от микросервисов, потому что по факту это будет распределенный монолит — набор сильно связанных друг с другом сервисов, которые способны работать только в совокупности и не могут существовать раздельно. Существует также важная проблема со сложностью распределенных систем. Очень часто возникают ситуации, когда необходимо гарантировать доставку данных до сервиса и получить ответ, — особенно это касается темы, связанной с финансовым оборотом. Каждый сервис при этом имеет собственную базу данных, и сохранять согласованность между данными приходится с помощью отдельных механизмов — двухфазных фиксаций, или саг (об этом будет подробно рассказано в главе 4). Доставка приложения до конечного пользователя хотя и ускоряется с помощью монолитов, однако предполагает использование большого количества сложных инструментов, таких как Docker, Kubernetes, CI/CD и ArgoCD, отдельных сервисов Vault для хранения секретных ключей и т. д. Мы коснемся темы развертывания в последней главе и рассмотрим, как это можно сделать с помощью Docker, DockerCompose, а также познакомимся поближе с CI/CD и Kubernetes на практике. Таким образом, выбор между монолитом и микросервисами, как и выбор фреймворка, сводится к вопросу о цели проекта. Если это долгоиграющая история на несколько лет с поддержкой кода, исправлением багов и доработкой новой функциональности, большими планами и командой, то стоит сразу закладывать возможность микросервисной архитектуры. В другом случае, когда надо простое минисайт приложение, которое достаточно опубликовать один раз и больше ничего в нем не менять, — вам хватит и монолита, а микросервисы только приведут к лишнему усложнию системы, которая того абсолютно не требует. ЭЛЕКТРОННЫЙ АРХИВ Электронный архив с примерами к книге выложен на сервер издательства «БХВ» по адресу: https://zip.bhv.ru/9785977519359.zip. Ссылка доступна и со страницы книги на сайте https://bhv.ru/ (см. приложение). Список литературы и источников 1. https://habr.com/ru/articles/740934/. 2. https://h.amazingsoftworks.com/ru/articles/679992/. 3. Крис Ричардсон. Микросервисы. Паттерны разработки и рефакторинга.


ГЛАВА 1 Разработка первого микросервиса (User) Настройка локального окружения Редактор кода и плагины Перед тем как приступить к разработке любого приложения на любом языке программирования, необходимо установить и настроить все необходимые для этого инструменты. И в первую очередь выбрать удобный редактор кода. Есть несколько самых распространенных: WebStorm, Visual Studio Code, Sublime Text, Atom, Brackets, Notepad++. Notepad++ и SublimeText — простенькие редакторы, которые необходимо настраивать самостоятельно — т. е. ставить нужные плагины, поскольку с их настройками, так сказать, «из коробки», работать будет сложно, да и во всём прочем они значительно уступают остальным. Visual Studio Code, Atom и Brackets — бесплатные редакторы c мощной функциональностью, такой как автодополнение, поддержка контроля версий, отладка. Наибольшую популярность из них приобрел Visual Studio Code за счет меньшего энергопотребления и нагрузки на компьютер. WebStorm от JetBrains предоставляет большинство необходимой функциональности с настройками «из коробки», правда, он платный, но для обучающихся и преподавателей есть возможность приобрести бесплатную лицензию. Рассматривать каждый из редакторов кода — слишком объемная задача, тем более что не она является нашей целью в этой книге, поэтому без лишней лирики остановим свой выбор на доступном и оптимальном варианте — Visual Studio Code. Первый запуск редактора выглядит следующим образом (рис. 1.1). Установим в нем расширения для Node.js. В строке поиска расширений введите: Node.js Extension Pack, найдите и установите этот пакет. Он включает расширение IntelliSense, которое предоставляет подсказки возможных вариаций завершения кода во время его написания, а также подсвечивает ошибки в синтаксисе и неверное использование переменных или операторов, расширение Node.js Debugger, обеспечивающее отладку на Node.js, а также менеджер управления зависимостями npm, помогающий в генерации шаблонов проектов, управлении запуском и остановкой приложений. Установить необходимо и ESLint — плагин для проверки в режиме реального времени того, что написанный код соответствует заранее определенному стилю


16 Глава 1 (codestyle). В командной разработке это первоочередный вопрос — определить настройки ESLint. Подробно возможные вариации мы рассмотрим позже, а сейчас только установим это расширение, впрочем, часто оно может быть уже предустановлено. С остальными моментами работы с редактором кода мы будем знакомиться по мере необходимости. Рис. 1.1. Первый запуск Visual Studio Code Node.js и пакетные менеджеры Чтобы установить непосредственно сам Node.js, есть несколько способов. Если вы используете macos или Linux, лучше всего это сделать с помощью пакетного менеджера. Все возможные варианты такой установки показаны на официальном сайте Node.js: https://nodejs.org/ru/download/package-manager. Можно установить Node.js и через установщик: https://nodejs.org/ru/download. Но для меня предпочтительнее первый вариант — из-за того, что через пакетный менеджер также просто удаляются все зависимости и пакеты. Проверить, что все установилось верно, можно с помощью команды в терминале: node -v В ответ должна вывестись версия установленного пакета Node.js — в моем случае это v.20.0.0.


Разработка первого микросервиса (User) 17 Как правило, вместе с установкой Node.js также устанавливается и npm. Убедиться в этом можно, открыв терминал и выполнив команду: npm -v В моем случае установлена версия 9.5.1, у вас она может быть выше. Npm — это официальный пакетный менеджер для Node.js, именно поэтому он и устанавливается по умолчанию вместе с ним. Суть всех пакетных менеджеров — в управлении зависимостями проекта. Npm имеет наибольшее сообщество и поддержку в основном потому, что был первым и остается официальным пакетным менеджером для проектов, в которых используется JavaScript. Помимо npm, есть еще один популярный пакетный менеджер — yarn. Yarn был разработан Facebook как альтернатива для npm, чтобы решить некоторые проблемы, связанные с производительностью и надежностью npm. Разберемся с их различиями: безопасность — yarn проверяет наличие уязвимостей в пакетах при установке, в то время как npm делает это только при загрузке зависимостей во время выполнения программы; поддержка плагинов — yarn позволяет добавлять плагины для расширения функциональности, такие как автоматическое обновление зависимостей или изменение конфигурации; в yarn предусмотрена специальная команда upgrade-interactive, которая позволяет взаимодействовать с интерактивным интерфейсом для обновления зависимостей в проекте прямо из терминала. Другой способ заключается в редактировании версии в файле package.json либо в обновлении пакетов вручную, что гораздо менее удобно. Также ранее как преимущество подчеркивалось, что yarn значительно быстрее npm при установке пакетов, — благодаря параллельной загрузке и кешированию зависимостей, однако функция кеширования пакетов была добавлена и в npm в пятой его версии, выпущенной в 2017 году. Некоторые из этих различий также реализованы и в npm, но с менее удобным использованием. Например, механизм фиксирования зависимостей присутствует и в npm — файл package-lock.json. Это файл, создаваемый npm при установке пакетов, в котором фиксируются точные версии зависимостей проекта и их деревьев зависимостей, чтобы гарантировать, что одинаковые версии пакетов будут установлены на разных машинах у разных разработчиков. Разница же с файлом yarn.lock заключается в разных форматах файлов, и yarn.lock, кроме того, обладает дополнительными возможностями, — например, способностью зафиксировать версию поддержки платформы (в нашем случае версию Node.js). Есть и еще один пакетный менеджер — pnpm, который был создан для решения проблем с управлением зависимостями. Главная его задача состоит в том, чтобы облегчить работу с большим количеством пакетов, уменьшить время их установки и объем занимаемого дискового пространства. В Node.js зависимости сохраняются в папку node_modules, и каждый раз, когда вы устанавливаете новый пакет, он до-


18 Глава 1 бавляется в эту папку. Но нюанс в том, что устанавливаемые библиотеки также чаще всего имеют зависимости, и в случае с npm и yarn, если одна и та же зависимость одинаковой версии используется в нескольких других устанавливаемых пакетах, эта зависимость будет установлена столько же раз. В результате получается, что в проекте оказывается большое количество дублирующего кода, а сам проект занимает намного больше места на диске. Это все приводит к дальнейшим проблемам при эксплуатации кода, таким как: длительное время доставки кода — чем больше размер проекта, тем больше времени требуется на его сборку и развертывание; высокое потребление ресурсов — большой размер проекта требует больше оперативной памяти и процессорного времени, что приводит к большим расходам на серверы и оборудование; проблемы с безопасностью — большое количество зависимостей может увеличить риски с безопасностью, т. к. каждый устанавливаемый пакет может иметь свои уязвимости. Еще одним важным преимуществом pnpm является скорость установки пакетов. Когда вы устанавливаете сразу все зависимости проекта, npm и yarn делают это по очереди, в то время как pnpm задействует локальный кеш, где сохраняются зависимости, установленные ранее [1]. Присутствуют в использовании pnpm и отрицательые моменты: возможные проблемы с кросс-платформенностью — на разных системах pnpm может вести себя по-разному, а связано это с тем, что технология, которая предотвращает дублирование пакетов, не на всех системах работает одинаково; неподдерживаемые пакеты — некоторые пакеты могут работать неправильно, потому что не поддерживают символические ссылки, которые используются в pnpm; небольшое сообщество — pnpm значительно менее популярен, чем npm или yarn, поэтому, если вы столкнетесь с проблемами, вероятность найти поддержку ниже. NVM Пакет NVM (Node Version Manager) не является обязательным для установки, но крайне желателен. Этот инструмент позволяет управлять установленными версиями Node.js. В основном это актуально, когда ведется работа сразу над несколькими проектами, и в них могут использоваться разные версии Node.js. Если говорить о том, как вообще происходит повышение версии в уже давно написанном проекте, то для этого отводится отдельная ветка, где версию и обновляют. Важный момент заключается в том, что нельзя сразу «прыгать» на несколько версий вперед — необходимо повышать версию поэтапно. Это связано с тем, что переход к другой версии может потребовать обновления зависимостей и библиотек, используемых в проекте, которые могут быть несовместимы с новой версией Node.js. Поэтому


Разработка первого микросервиса (User) 19 после каждого этапа повышения версии необходимо проводить полноценное тестирование всей функциональности проекта. Для того чтобы установить nvm на Linux или macOS, нужно выполнить ряд шагов: 1. Открыть терминал и выполнить команду для скачивания скрипта установки: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 2. Закрыть и заново открыть терминал либо выполнить команду обновления текущей сессии терминала: source ~/.bashrc 3. Проверить, что nvm установлен, с помощь команды: nvm –version Если в терминале вывелась версия nvm, значит, все установлено правильно. Установить нужную версию Node.js можно с помощью команды: nvm install 20.0.0 После установки версия Node.js будет выбрана автоматически. Проверить, что версия Node.js установлена правильно, можно командой: node -v Переключаться между уже установленными версиями Node.js можно с помощью команды: nvm use <нужная версия> Установка NestJS Так как пакетный менеджер у нас уже установлен — как минимум это npm, с его помощью мы можем далее установить необходимые зависимости. У NestJS есть собственный инструмент для работы через терминал — Nest CLI (Command Line Interface). Он позволяет легко и быстро создавать новые проекты на NestJS, генерировать файлы и отдельные компоненты внутри этих проектов. Установка NestJS производится с помощью команды: npm i -g @nestjs/cli где i — это сокращение для команды install, а флаг -g говорит о том, что необходимо установить этот пакет глобально, т. е. установка произойдет на уровне операционной системы, а не в конкретном проекте. Таким образом, пакет будет доступен для использования из любого каталога в терминале. Правда, есть нюанс — глобальная установка может приводить к конфликтам версий и переопределению зависимостей между проектами. Далее с помощью установленного Nest CLI мы можем создать проект одной командой: nest new project-name


20 Глава 1 Эта команда создает новый проект со стандартной структурой каталогов и файлов, необходимых для начала работы. Есть также отдельные команды для добавления модулей (NestJS использует модульную структуру организации приложения) и генерации компонентов — таких как контроллеры или сервисы. Если вы не уверены, есть ли уже команда для нужного вам действия, всегда можно вызвать команду: nest -help. Она выводит список доступных команд Nest CLI и их краткое описание (рис. 1.2). Также можно использовать ее с указанием конкретной команды, чтобы получить подробную справочную информацию и быстро ознакомиться с основными функциями и возможностями инструмента. Подробнее с функционалом Nest CLI можно ознакомиться в документации по ссылке: https://nestjs.ru/cli/overview. Рис. 1.2. Вызов команды nest -help В целом NestJS имеет обширную документацию, в которой описаны все основные применяемые концепции и возможности с примерами использования. К тому же имеется активное сообщество разработчиков, а значит, находить ответы на возни-


Разработка первого микросервиса (User) 21 кающие вопросы будет значительно легче. Стоит также отметить, что подтверждением актуальности и востребованности NestJS является количество еженедельных скачиваний пакета — более миллиона в неделю на текущий момент. Это также важный показатель, на который стоит обращать внимание при выборе того или иного пакета для своего проекта, потому что чем больше разработчиков им пользуются, тем меньше там уязвимостей, — они быстрее находятся и подсвечиваются для разработчиков фреймворка. Git Git — это система контроля версий, которая позволяет управлять историей изменений в проекте. Для установки Git нужно перейти на ее официальный сайт по ссылке: https://git-scm.com/, и скачать установщик для своей операционной системы. А после установки проверить, что Git установлена корректно, выполнив команду в терминале: git –-version Рассмотрим базовые концепции Git и то, как они могут применяться в процессе разработки приложений на Node.js. Создание репозитория. Первое, с чего начинается работа над любым проектом, — создание репозитория. Это история развития проекта и хранилище всех его версий. Для создания репозитория необходимо выполнить команду git init, которая создаст пустой репозиторий в текущем каталоге. Выглядит это как созданная папка .git, в которой содержатся все необходимые объекты для работы Git. Добавление файлов. Для отслеживания изменений в файлах необходимо их проиндексировать. Это можно сделать командой git add. Вы можете добавлять по одному файлу или сразу несколько файлов за раз. Так, команда git add. добавляет в индекс сразу все файлы из текущего каталога. Коммиты. Сохранение изменений в репозитории Git называется коммитом (commit). Команда git commit создает новый коммит, содержащий все изменения, добавленные в индекс. При создании коммита необходимо указать комментарий, как правило, это краткое описание того, что было сделано. Существует рекомендованный набор правил для написания комментариев в коммите — conventional commits. Он нужен в первую очередь для стандартизации формата сообщений, чтобы другим разработчикам было проще понять, что было изменено или исправлено. На их основе также делают скрипты для автоматической генерации информации о выпущенных версиях и изменениях, которые были внесены. Ветки. Ветки в Git позволяют вести параллельную работу над проектом и разделять историю коммитов на отдельные линии разработки. Команда git branch создает


22 Глава 1 новую ветку, а команда git checkout позволяет переключаться между ветками. Это полезно, когда ведется длительная разработка над новой функциональностью в отдельной ветке, а вам срочно нужно «починить баг» в основной версии, поэтому вы можете сохранить изменения в своей ветке и безболезненно переключиться на главную (мастер) версию. Слияние (merge). Эта функциональность представляет собой объединение изменений из одной ветки в другую. Реализуется она командой git merge. При слиянии Git объединяет изменения из обоих веток, сохраняя при этом историю коммитов. Слияние может не завершиться автоматически, если в одни и те же файлы были внесены изменения в разных ветках, — тогда необходимо «вручную» разрешить эти конфликты, т. е. выбрать, какие изменения в файле должны быть в итоге внесены. Сейчас большинство редакторов кода наглядно подсвечивают конфликтные места и предлагают варианты решения. Для удобной работы с Git рекомендуется использовать графический интерфейс или терминал. Одна из важных задач Git — отслеживание изменений в коде, а также контроль версий. Благодаря этому, не так страшно допустить ошибку, потому что всегда можно вернуться на предыдущую версию. Также Git помогает в совместной работе над проектом, позволяя нескольким разработчикам работать с одним кодом. Важно понимать, что Git — это не только инструмент для контроля версий, но и способ организации рабочего процесса в команде. Процесс и правила для организации работы с репозиторием и управлением ветвлением для Git называются git flow. В первую очередь рекомендуется использовать git flow для сложных и больших проектов, которые имеют длительный жизненный цикл и множество разработчиков. В этой модели определены следующие ветки: Master (main) — ветка с готовым к релизу кодом; Develop — ветка, на которой ведется разработка следующей версии продукта. Веток Develop может быть несколько — для каждого разработчика или команды, а также для выкатки кода на develop-стенд с целью проверки работоспособности и интеграций, поскольку бывают ситуации, когда невозможно проверить все локально; Feature — ветка, создаваемая для разработки новой функциональности. Ветки Feature должны отделяться от ветки Develop и сливаться обратно с ней после завершения работы; Release — релизная ветка, создаваемая для подготовки к выпуску новой версии продукта. В этой ветке производится исправление багов и подготовка документации. Release-ветка создается из ветки Develop и вливается в Master и Develop после ее завершения; Hotfix — ветка, создаваемая для решения критических багов в готовом продукте, обнаруженных после его релиза. Ветки Hotfix создаются из ветки Master и сливаются обратно в Master и Develop. Как правило, в проектах применяется git flow в той или иной редакции, причем команды часто адаптируют эти правила под себя. Например, все чаще применяется


Разработка первого микросервиса (User) 23 практика, когда есть еще ветки test и preprod, которые отвечают за релизы на тестовых стендах. В больших проектах мало иметь для продакшена один стенд, поскольку для полноценного тестирования необходимо где-то еще разворачивать код, чтобы тестировщики могли проверить работоспособность и обнаружить возможные баги. Поэтому к приведенному списку добавляется еще несколько веток, отвечающих за выкатку кода на соответствующие стенды: Test — ветка, на которой проводится тестирование тестировщиками, она соответствует тестовому стенду; Preprod — ветка, с полноценной функциональностью, максимально приближенная к продакшену. Чаще всего используется для промежуточных показов заказчику и другим заинтересованным лицам. Отдельно стоит отметить, что для Git есть три типа файлов: отслеживаемые, неотслеживаемые и игнорируемые. Файл считается отслеживаемым, если он проиндексирован или зафиксирован в коммите. Неотслеживаемый файл — это файл, который не проиндексирован. Так, при создании нового файла по умолчанию он считается не проиндексированным, но как только мы его проиндексируем, он станет считаться отслеживаемым. Игнорируемый файл — это файл, явным образом помеченный для Git как файл, который необходимо игнорировать. Это, как правило, установленные зависимости, артефакты сборки, скрытые системные файлы (.DS_Store), файлы, относящиеся к настройкам вашей IDE, и любые произвольные файлы, которые вы посчитаете нужным не распространять вместе с проектом. Игнорируемые файлы отмечаются в специальном файле .gitignore, который располагается в корневом каталоге проекта. Чтобы добавить игнорируемые файлы, необходимо вручную в этом файле их прописать — специальной команды для этого в Git нет. Игнорируемые файлы необходимы, потому что не стоит загружать в удаленный репозиторий Git абсолютно все из проекта, поскольку, во-первых, в таком случае репозиторий станет очень «тяжелым», а во-вторых, постоянно понадобится решать конфликты при слиянии, если вы работаете еще с кем-то. Codestyle Codestyle — это соглашения и правила форматирования кода, которые определяются для улучшения читаемости и поддержки кода. Единый способ форматирования кода не был выработан сообществом, поэтому в каждом проекте все команды договариваются между собой, как им удобно. Желательно как можно раньше озадачиться этим вопросом, т. к. стандарты помогают всем участникам проекта писать код в едином стиле, что повышает понимание и облегчает совместную работу. В результате должно быть неважно, кто именно работал над проектом, и код не должен отличаться в зависимости от предпочтений его автора. Рассмотрим некоторые примеры правил, присутствующие в каждой команде в разных редакциях. Использование отступов. Отступ в 4 пробела считается стандартом и наиболее распространен, но применяются отступы и в 2 «таба». Отступы помогают улучшить читабельность кода и


24 Глава 1 выделяют блоки кода, вложенные друг в друга. Желательно также оставлять пустые строки для разделения блоков кода по смысловому содержанию. Например, если у вас получение каких-либо данных занимает пять строчек кода, то после них рекомендуется сделать пустую строку, после которой будет идти код уже с другим назначением. Именование переменных. Хорошими именами переменных считаются краткие, описательные и уникальные названия. В Node.js для именования переменных принято использовать стиль camelCase, если требуется больше одного слова. Правила расположения скобок. Фигурные скобки чаще всего открываются на той же строке, что и код, а закрываются на отдельной, — что позволяет наглядно выделить связный кусок кода. Длина строки. Предпочтительно чтобы длина строки не превышала 80–100 символов, но сейчас этот стандарт сдвигается ближе к 140 символам — это связано с увеличением экранов и их вместительности. В целом главное, чтобы строка кода помещалась полностью на экране у всех разработчиков команды. Отсутствие магических чисел. Лучше использовать константу или переменные для значений, которые используются больше одного раза, либо фиксированные значения, чтобы код был более читабельным. Если оставить просто число без пояснения, другому разработчику может быть непонятно, откуда оно взялось и почему принято именно такое значение. В случае с константой это решается названием переменной, которая должна указывать на то, что это число обозначает. Второй вариант решения — оставить комментарий. Использование комментариев. Если вам кажется, что какой-то кусок кода может быть понят неоднозначно, или вы используете временное или спорное решение, желательно этот момент прокомментировать, чтобы другим разработчикам или вам же в будущем было понятно, почему для реализации принято именно такое решение. Стоит отметить, что эти правила не единственные, но наиболее применимые ко всем проектам и стекам технологий, однако в некоторых случаях они все же могут различаться. Поэтому важно выбрать или изучить принятый стандарт и следовать ему в рамках конкретного проекта. Docker и Docker-Compose Docker — это инструмент для контейнеризации приложений. Он позволяет упаковывать приложения и их зависимости в изолированные контейнеры, которые можно переносить на любую платформу, и в любой среде, независимо от хост-системы и окружения, приложение будет работать одинаково.


ГЛАВА 2 Разработка микросервиса авторизации и аутентификации (Auth) Теоретический обзор способов авторизации и аутентификации Аутентификация, идентификация и авторизация При разработке приложений несколько более сложных, чем одностраничные (Single Page Application, SPA), особенно если там еще нужна и микросервисная архитектура, весьма высока вероятность, что придется работать с данными пользователей. Соответственно, необходимо обеспокоиться гарантией того, что никто не сможет получить к чужим данным несанкционированный доступ. Для начала разберемся с терминами. Идентификация — предоставление информации о том, кем вы являетесь. Это может быть что угодно: имя, адрес электронной почты, номер телефона, идентификатор учетной записи и т. д. Стоит отметить, что в момент, когда вы заполняете форму анкеты на каком-либо сайте, вы идентифицируете себя в качестве кого-то. При этом, даже если вы не нажали кнопку «сохранить», это все равно будет считаться идентификацией. Аутентификация — предоставление доказательств того, что вы на самом деле есть тот, кем идентифицировались. То есть кто-то должен подтвердить, что вы именно тот, за кого себя выдаете. Это может быть осуществлено, если вы предоставите документ, удостоверяющий личность, и контролер удостоверится, что на фотографии в документе действительно вы, а документ при этом не поддельный. Авторизация — проверка того, что вам разрешен доступ к запрашиваемому ресурсу. То есть если вы хотите удалить, например, нарушающий правила форума комментарий, системе перед этим необходимо убедиться, что вы как минимум являетесь модератором этого форума и вам разрешен доступ на совершение указанного действия. Если говорить про способы аутентификации в веб-среде в таких терминах, то традиционно под идентификацией понимают получение вашей учетной записи по


80 Глава 2 имени пользователя (username), а под аутентификацией — проверку того, что вы знаете пароль от этой учетной записи. Авторизацией же будет считаться проверка вашей роли в системе и решение о предоставлении доступа к запрашиваемому ресурсу. Рассмотрим подробнее способы аутентификации и авторизации [1]. Аутентификация по паролю Этот метод основан на том, что при регистрации пользователь задает имя пользователя (логин) и пароль, а при повторном входе для успешной идентификации и аутентификации в системе их предъявляет. Часто в качестве имени пользователя используется адрес электронной почты. Прежде всего, при аутентификации по логину и паролю используется стандартный HTTP/HTTPS-протокол. Это работает следующим образом: 1. При запросе неавторизованного клиента на доступ к ресурсу сервис отвечает статусом 401 «Unauthorized» и добавляет заголовок «WWW-Authenticate» с указанием схемы и параметров аутентификации. 2. Браузер, получив такой ответ, перенаправляет посетителя на страницу авторизации, чтобы посетитель ввел свои логин (username) и пароль (password). 3. Во всех следующих запросах к этому веб-сайту браузер добавляет заголовок «Authorization», в котором передаются необходимые данные для аутентификации посетителя сервером. 4. Сервер аутентифицирует посетителя по данным из этого заголовка на каждый запрос и принимает решение о том, предоставлять ему или нет доступ к запрашиваемому ресурсу (авторизация). При этом существует несколько схем аутентификации, различающихся по уровню безопасности: Basic — username/password пользователя передаются в заголовке «Authorization» в незашифрованном виде (base64-encoded). Впрочем, при использовании HTTPSпротокола эта схема является относительно безопасной; Digest — challenge/response схема, при которой сервер посылает уникальное значение nonce (чаще всего оно представляет собой временну´ю метку и, возможно, какую-то еще добавочную информацию, что не обязательно), а браузер передает MD5 хеш пароля пользователя, который вычисляется с использованием указанного nonce; схемы NTLM и Negotiate применяются для пользователей домена Windows, что не способствует широкому их распространению. Стоит отметить, что при использовании HTTP-аутентификации у пользователя нет стандартной возможности выйти из веб-приложения, кроме как закрыть все окна браузера. Описанный здесь способ аутентификации пользователей применяется для вебсайтов, однако при разработке систем более широкого применения (с участием


Разработка микросервиса авторизации и аутентификации (Auth) 81 мобильной разработки — например, для iOS или Android) наряду с HTTP-аутентификацией часто используются и другие способы, где данные для аутентификации передаются в других частях запроса. Например, логин и пароль также можно передать в HTTP-запросах: в URL query — при использовании HTTP, а не HTTPS, это считается небезопасным, поскольку строки URL могут запоминаться браузерами и веб-серверами. Кроме того, такие данные могут быть перехвачены злоумышленником; в Request body — безопасный вариант, но применяется только для POST-, PUTи PATCH-запросов, где задействуется тело сообщения; в HTTP header — оптимальный вариант, при этом могут использоваться и стандартный заголовок «Authorization» вместе с Basic-схемой, и другие пользовательские заголовки. Аутентификация по сертификатам Идентификационный документ (сертификат), заверенный органом по сертификации (Certificate Authority, CA), содержит характеристики, которые дают возможность опознать его владельца. Роль CA аналогична роли посредника, обеспечивающего подтверждение подлинности сертификатов (по аналогии с органами, выдающими паспорта). Кроме того, криптографически связанный с закрытым ключом сертификат находится в собственности владельца документа и позволяет четко установить, что документ принадлежит ему. На стороне пользователя сертификат и соответствующий закрытый ключ могут храниться в операционной системе, браузере, файле или на физическом устройстве (например, смарт-карте, USB-токене). Как правило, закрытый ключ дополнительно защищен паролем или PIN-кодом. В контексте веб-приложений часто используются сертификаты формата X.509. Проверка подлинности с помощью сертификата X.509 происходит при установлении связи с сервером и интегрирована в протокол SSL/TLS. Этот метод также хорошо поддерживается веб-браузерами, позволяя пользователям выбирать и использовать сертификаты, если веб-сайт поддерживает такой способ аутентификации. При этом сертификат проверяется сервером на основании следующих правил: сертификат должен быть подписан доверенным органом по сертификации (CA); сертификат должен быть действительным на текущую дату (осуществляется проверка срока действия); сертификат не должен быть отозван соответствующим CA (производится проверка списков исключения). После успешной проверки подлинности сертификата веб-приложение может принять решение, разрешить ли доступ на основе данных из сертификата — таких как имя владельца, эмитент (тот, кто выдал сертификат), серийный номер или отпечаток открытого ключа.


82 Глава 2 Применение сертификатов для проверки подлинности — более надежный способ, чем использование паролей. Его надежность обеспечивается благодаря цифровой подписи, которая создается при проверке подлинности и подтверждает использование определенного закрытого ключа (невозможность отрицания). Однако сложности с распространением и поддержкой сертификатов делают этот способ не столь доступным для многих пользователей. Аутентификация по одноразовым паролям Аутентификация с использованием вре´менных паролей зачастую дополняет обычную аутентификацию паролями, образуя двухфакторную аутентификацию (2FA). В этом методе пользователь обязан подтвердить как обычный пароль (который он знает), так и уникальный временный пароль. Применение двух факторов позволяет повысить безопасность, что особенно актуально для приложений, связанных с финансовыми активами, хотя и не ограничивается только ими. Помимо использования для обеспечения доступа к ресурсам, одноразовые пароли могут потребоваться для подтверждения личности при совершении значимых операций — таких как денежные переводы, подтверждение покупок, продление подписок или изменение аккаунтных настроек. В последнее время временные пароли также нашли свое применение при подключении к корпоративным виртуальным частным сетям (VPN), повышая уровень безопасности и обеспечивая защиту конфиденциальной информации. Для генерации одноразовых паролей есть разные способы, вот наиболее распространенные из них: Аппаратные или программные токены. Стандартный аппаратный токен — это небольшое устройство, обычно представленное в виде кредитной карты или брелока для ключей. Простейшие аппаратные токены похожи на USB-флешки (их часто называют донглами) и содержат сертификат или уникальный идентификатор. У более сложных токенов могут иметься ЖК-дисплеи, клавиатуры для ввода паролей, биометрические считыватели, устройства беспроводной связи и дополнительные функции для повышения безопасности [1]. C технической точки зрения информация для аутентификации может передаваться в различных частях HTTP-запроса (наиболее оптимальный вариант — header). В некоторых случаях для передачи токена могут использоваться заголовки Bearer. Чтобы избежать перехвата ключей, соединение с сервером должно быть обязательно защищено протоколом SSL/TLS. Случайно генерируемые коды, передаваемые пользователю через SMS или другой канал связи. В этой ситуации фактор владения — SIM-карта, привязанная к определенному номеру. Сейчас также активно используются способы типа Google Authenticator — их принцип работы заключается в том что генерируется одноразовый код, который действует очень короткий промежуток времени (часто меньше минуты).


Способы взаимодействия между микросервисами 115 Одно подключение может иметь множество каналов. Однако каждый канал занимает некоторые структуры и объекты в памяти, поэтому чем больше каналов будет создано в рамках одного соединения, тем больше памяти понадобится RabbitMQ для управления этим соединением. С учетом сказанного открывать новое соединение для каждой операции все же не рекомендуется, потому что это приводит к большим затратам ресурсов. Впрочем, некоторые ошибки протокола могут приводить к закрытию канала, поэтому срок работы канала может быть меньше, чем у соединения. Помимо использования RabbitMQ в качестве брокера сообщений для взаимодействия между микросервисами, его также часто применяют для фоновой обработки данных. Как правило, к этому прибегают, когда необходимо сгенерировать какойто большой документ, процесс генерации которого занимает продолжительное время. При этом очевидно, что HTTP-соединение точно не дождется генерации и упадет по тайм-ауту, так что получившийся файл по готовности кладут в RabbitMQ и ставят в очередь на отправку получателю, либо сообщают ему ссылку на его скачивание по почте, либо множеством других способов оповещают получателя о его готовности. В таких ситуациях RabbitMQ служит шиной события и позволяет вебсерверам заниматься основными запросами, а не выполнять трудоемкие задачи в моменте. Apache Kafka Apache Kafka — это еще один брокер сообщений, такой же как RabbitMQ, и предназначение у обоих глобально одинаковое, однако есть между ними и существенные различия. Первое различие, с которого хотелось бы начать, — у Kafka и RabbitMQ разные модели запросов. Как известно, существуют два типа моделей запросов: pull и push [5]: согласно pull-модели, подписчики (consumer) сами отправляют запрос один раз в несколько секунд на сервер для получения новых сообщений. Плюсом такого подхода является возможность группировать сообщения при получении в батчи и достигать таким образом большей пропускной способности. Однако при этом обработка данных будет происходить с некоторой задержкой, и потенциально может возникнуть разбалансировка между разными подписчиками; согласно push-модели сервер делает запрос к клиенту, отправляя ему новые сообщения. При этом снимается время обработки данных, что позволяет контролировать сбалансированное распределение сообщений между подписчиками. А для того чтобы у подписчиков не возникла перегрузка, приходится выставлять лимиты, используя функциональность QS1 . В Kafka используется pull-модель, тогда как RabbitMQ использует push-модель. Помимо моделей запросов, эти инструменты также различаются механизмами хранения и потребления сообщений на брокере. В отличие от некоторых других сис- 1 См. https://www.rabbitmq.com/maxlength.html.


116 Глава 3 тем, в Kafka сообщения сохраняются даже после обработки подписчиками, и они могут там пребывать до неопределенного момента. Такой подход позволяет обрабатывать одно и то же сообщение множеством подписчиков в различных контекстах. Несмотря на это, разработчики Kafka предусмотрели механизмы, предотвращающие проблемы с повторным чтением одних и тех же сообщений одним и тем же подписчиком. Для того чтобы разобраться в этом подробнее, рассмотрим, как устроена Kafka. Сообщения, притекающие от «производителей» (producers) — т. е. от других процессов, — хранятся в Kafka в формате «ключ-значение». Кроме того, данные могут быть разделены на разделы (partitions) внутри различных тем (topics). Эти сообщения строго упорядочены внутри каждого раздела на основе их смещения (offset) — позиции внутри раздела, и также индексируются и сохраняются вместе со временем создания. Это и позволяет не читать одни и те же сообщения множество раз. Клиентские приложения, известные как «потребители» (consumers), способны осуществлять чтение сообщений из этих разделов. Потоковое Streams API в Kafka облегчает разработку приложений, которые могут не только получать данные из Kafka, но и записывать их обратно. Этот интегрированный инструмент также отлично взаимодействует с различными внешними системами обработки потоков — такими как Apache Apex, Apache Beam, Apache Flink, Apache Spark, Apache Storm и Apache NiFi. Kafka функционирует в распределенном кластере, состоящем из одного или нескольких узлов-брокеров, на которых размещены разделы всех тем. Для обеспечения надежности и отказоустойчивости разделы реплицируются на нескольких брокерах. Начиная с версии 0.11.0.0, система предоставляет возможность задействовать транзакционную модель, подобную той, которая используется в базах данных. Это позволяет обрабатывать поток данных с помощью Streams API только один раз. Концепция тем в Kafka включает два их типа: обычные и компактные: обычные темы можно настроить с определенным сроком хранения или ограничениями по занимаемому пространству. Если лимит пространства превышен, система автоматически удаляет самые старые записи. Записи, чей срок хранения истек, также будут удалены, независимо от лимитов памяти; в свою очередь, компактные темы не обладают сроком хранения или ограничениями по памяти. Вместо этого Kafka сохраняет и обрабатывает только последние значения для каждого ключа и гарантирует, что ни одно последнее сообщение для каждого ключа никогда не будет удалено. Пользователи могут также вручную удалять сообщения, отправляя их с пустым значением для удаления записи по ключу. Kafka Connect, также известный как Connect API, представляет собой мощный фреймворк для безупречного импорта данных из внешних систем и безукоризненного экспортирования данных в другие системы. Этот ценный инструмент впервые был представлен в версии Kafka 0.9.0.0. В рамках фреймворка Connect создаются так называемые коннекторы, которые берут на себя всю логику чтения и записи данных во внешние системы [6].


Способы взаимодействия между микросервисами 117 Чтобы обеспечить гибкость и разнообразие, Connect API определяет программный интерфейс, который может быть реализован для различных языков программирования. Поэтому существуют уже готовые реализации API для большинства популярных языков. Это позволяет разработчикам работать на предпочитаемом языке без необходимости заботиться о реализации самого API. Важно отметить, что компания Apache Kafka не участвует напрямую в разработке конкретных библиотек для различных языков программирования. Однако благодаря открытой структуре Connect API сообщество разработчиков активно создает и поддерживает реализации API для разнообразных языков, обогащая возможности Kafka Connect и обеспечивая широкий спектр интеграций с другими системами. Redis Redis представляет собой организованное в оперативной памяти устройства хранилище типа «ключ-значение». Весьма часто этот инструмент задействуется в дополнение к уже рассмотренным нами ранее. Дело в том, что в базах данных вроде PostgreSQL, MySQL и MongoDB, используемых как долговременные хранилища, данные изменяются достаточно редко. Redis же как раз закрывает потребность в хранении где-то данных, к которым необходимо постоянно обращаться, и которые часто могут изменяться. Это помогает повысить скорость работы системы, поскольку обращение за данными в расположенный в оперативной памяти Redis происходит значительно быстрее, чем если пытаться получить эту информацию из PostgreSQL. Однако, хранение данных в оперативной памяти обходится значительно дороже, чем использование жесткого диска для хранения информации, поэтому Redis не может заменить классические базы данных, а является только дополнением к ним [7]. Сначала его использовали, как и Memcached, для кеширования данных в оперативной памяти, к которой может обращаться множество доступных серверов. Позже, по мере развития Redis, ему нашли применение и во многих других ситуациях, — например, при работе с очередями или в потоковой обработке данных. Сейчас Redis поддерживает множество структур данных: строки; хеш-таблицы; битовые массивы; списки; множества; битовые поля; упорядоченные множества; геопространственные данные; структуры HyperLogLog; потоки.


118 Глава 3 По большей части Redis применяют для хранения некоторых данных, которые можно разделить на следующие группы: редко меняющиеся данные, к которым часто обращаются (например, хедеры, требующие проверки при запросах, различные api-key); часто меняющиеся данные, не относящиеся к критически важным (постоянно запрашиваемые сгенерированные отчеты, которые каждый раз генерировать заново слишком накладно). Для наглядности, чтобы разобраться в том, как устроен Redis, настроим его в сервисе Auth (этой настройке и посвящен следующий раздел). Разработка интеграции сервисов Auth и Account Фактически Auth и Account уже интегрированы между собой через HTTP-запросы, однако можно несколько улучшить это взаимодействие, воспользовавшись возможностями Redis. Мы его применим для сохранения userId и login — чтобы не ходить каждый раз в сервис Account и не получать userId пользователя из базы данных. При этом добавим удаление пользователя из базы Redis, когда хотим удалить его совсем из системы, — чтобы не возникло ситуации, когда несуществующий клиент продолжает пользоваться ресурсами системы. Начать стоит с обновления Docker-Compose — чтобы запустить локально в контейнере нужные инструменты. Причем можно создать отдельный Docker-Compose для Redis, а можно добавить его в существующий в качестве еще одного сервиса. Однако тогда придется удалить уже развернутый контейнер и запустить его заново, вследствие чего мы потеряем все данные, уже имеющиеся в базе PostgreSQL. Но учитывая, что там нет сейчас никакой ценной информации, соберем сервисы в один файл: Account: docker/docker-compose.yaml version: "3.9" services: postgres: image: postgres:latest container_name: account-db ports: [ "5132:5432" ] env_file: [ .env ] volumes: - ./volumes/postgres:/var/lib/postgresql/data - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql restart: unless-stopped # Redis redis: container_name: book-redis


ГЛАВА 4 Разработка модуля Transaction Проектирование базы данных В главе 1 мы уже начинали проектировать базу данных и рассмотрели три нормальные формы базы данных, но есть и другие нормальные формы, разговор о которых мы здесь и продолжим [1]. Нормальная форма Бойса — Кодда (НФБК) — частная форма третьей нормальной формы Определение 3НФ не совсем подходит для следующих отношений: отношение имеет два или более потенциальных ключа; два и более потенциальных ключа являются составными; они пересекаются, т. е. имеют хотя бы один общий атрибут. Для отношений с одним потенциальным ключом, который служит первичным, НФБК соответствует 3НФ. Отношение считается находящимся в НФБК, если каждая нетривиальная и неприводимая функциональная зависимость слева содержит потенциальный ключ в качестве своего детерминанта. Например, когда рассматривается отношение, представляющее данные о бронировании стоянки на день (табл. 4.1). Таблица 4.1. Стоянки Номер стоянки Время начала Время окончания Тариф 1 09:30 10:30 Бережливый 1 11:00 12:00 Бережливый 1 14:00 15:30 Стандарт 2 10:00 12:00 Премиум-В 2 12:00 14:00 Премиум-В 2 15:00 18:00 Премиум-А


138 Глава 4 Атрибут «Тариф» имеет здесь уникальное название и зависит от выбранной стоянки и наличия льгот, в частности: «Бережливый»: стоянка 1 для льготников; «Стандарт»: стоянка 1 для не льготников; «Премиум-А»: стоянка 2 для льготников; «Премиум-B»: стоянка 2 для не льготников. Таким образом, возможны следующие составные первичные ключи: {Номер стоянки, Время начала}, {Номер стоянки, Время окончания}, {Тариф, Время начала}, {Тариф, Время окончания}. Это отношение вроде бы демонстрирует соответствие третьей нормальной форме (3НФ). Вторая нормальная форма (2НФ) тоже соблюдается, т. к. все атрибуты являются частью какого-либо потенциального ключа, и нет атрибутов, не входящих в ключ. Также отсутствуют транзитивные зависимости, что соответствует требованиям 3НФ. Тем не менее следует отметить функциональную зависимость «Тариф → Номер стоянки», в которой левая часть (детерминант) не является потенциальным ключом отношения, что делает это отношение несоответствующим нормальной форме Бойса — Кодда (НФБК). Недостаток такой структуры заключается в возможности случайно присвоить тариф «Бережливый» к бронированию второй стоянки, хотя он должен относиться только к первой стоянке. Это может привести к ошибкам и нежелательным последствиям в управлении данными. Можно улучшить структуру с помощью декомпозиции отношения на два («Тарифы» (табл. 4.2) и «Бронирование» (табл. 4.3)) и добавления атрибута «Имеет льготы», получив отношения, удовлетворяющие НФБК. Таблица 4.2. Тарифы Тариф Номер стоянки Имеет льготы Бережливый 1 Да Стандарт 1 Нет Премиум-А 2 Да Премиум-В 2 Нет Таблица 4.3. Бронирование Тариф Время начала Время окончания Бережливый 09:30 10:30 Бережливый 11:00 12:00 Стандарт 14:00 15:30


Разработка модуля Transaction 139 Таблица 4.3 (окончание) Тариф Время начала Время окончания Премиум-В 10:00 12:00 Премиум-В 12:00 14:00 Премиум-А 15:00 18:00 Четвертая нормальная форма Отношение успешно достигает четвертой нормальной формы (4НФ), если оно находится в нормальной форме Бойса — Кодда (НФБК) и все многозначные зависимости на самом деле представляют собой функциональные зависимости от их потенциальных ключей. Для иллюстрации рассмотрим отношение R (A, B, C). Если существует многозначная зависимость R.A → → R.B, то это означает, что множество значений B, соответствующее каждой паре значений A и C, зависит только от A и не зависит от C. Допустим, у нас есть данные о ресторанах, производящих различные виды пиццы, и о службах доставки, которые работают только в определенных районах города. Переменная отношения имеет составной первичный ключ, включающий три атрибута: {Ресторан, Вид пиццы, Район доставки}. Однако такая переменная отношения не удовлетворяет 4НФ, т. к. имеется следующая многозначная зависимость: {Ресторан} → {Вид пиццы} {Ресторан} → {Район доставки} Это означает, что при добавлении нового вида пиццы необходимо будет добавить по одной новой записи для каждого района доставки. Такая ситуация может привести к логическим аномалиям, когда определенному виду пиццы будут соответствовать лишь некоторые районы доставки из обслуживаемых рестораном районов. Чтобы предотвратить отмеченную аномалию, необходимо декомпозировать отношение, разместив независимые факты в разных отношениях. В этом примере следует выполнить декомпозицию на {Ресторан, Вид пиццы} и {Ресторан, Район доставки}. Однако если к исходной переменной отношения добавить атрибут, функционально зависящий от потенциального ключа, — например, цену с учетом стоимости доставки ({Ресторан, Вид пиццы, Район доставки} → Цена), то полученное отношение будет удовлетворять 4НФ и его уже нельзя будет декомпозировать без потерь. Пятая нормальная форма Отношения, удовлетворяющие пятой нормальной форме (5НФ), достигают этого статуса только после выполнения требований четвертой нормальной формы (4НФ) и отсутствия сложных зависимых соединений между атрибутами.


140 Глава 4 Суть сложных зависимых соединений заключается в том, что если «Атрибут_1» зависит от «Атрибута_2», а, в свою очередь, «Атрибут_2» зависит от «Атрибута_3» и «Атрибут_3» зависит от «Атрибута_1», то все три атрибута обязательно должны быть объединены в один кортеж. Такое требование представляет собой очень строгое ограничение, и на практике редко можно встретить примеры реализации этого требования в чистом виде. Допустим, у нас есть таблица с атрибутами «Поставщик», «Товар» и «Покупатель». «Покупатель_1» покупает несколько товаров у «Поставщика_1». «Покупатель_1» также покупает новый товар у «Поставщика_2». В соответствии с требованием 5НФ «Поставщик_1» обязан поставлять «Покупателю_1» тот же самый новый товар, а «Поставщик_2» должен предоставить «Покупателю_1», помимо нового товара, всю номенклатуру товаров «Поставщика_1». Однако в реальной жизни покупатель свободен в своем выборе товаров. В связи с этим для преодоления указанной сложности все три атрибута разделяются по разным отношениям (таблицам). После выделения трех новых отношений («Поставщик», «Товар» и «Покупатель») при извлечении информации (например, о покупателях и товарах) необходимо в запросе соединить все три отношения. Важно не применять комбинацию соединения двух отношений, т. к. это может привести к извлечению некорректной информации. Зависимости между четырьмя, пятью или более атрибутами, соответствующие пятой нормальной форме, на практике встречаются крайне редко, и показать такие примеры практически невозможно. Доменно-ключевая нормальная форма Переменная отношения достигает Доменно-ключевой нормальной формы (ДКНФ), если каждое наложенное на нее ограничение является логическим следствием ограничений доменов и ограничений ключей, примененных к этой переменной отношения. Ограничение домена предписывает использовать для определенного атрибута только значения из заданного домена — перечня допустимых значений того или иного типа с указанием, что соответствующий атрибут должен иметь определенный тип данных. Ограничение ключа утверждает, что некоторый атрибут или комбинация атрибутов является потенциальным ключом. Любая переменная отношения, удовлетворяющая ДКНФ, автоматически соответствует и пятой нормальной форме (5НФ). Тем не менее не все переменные отношения можно привести к ДКНФ, т. к. это требование представляет собой жесткое ограничение, и на практике не всегда возможно его выполнение. Шестая нормальная форма Когда переменная отношения находится в шестой нормальной форме (6НФ), она удовлетворяет всем нетривиальным зависимостям соединения. Таким образом, переменная является неприводимой и не может быть декомпозирована без потерь.


ГЛАВА 5 Развертывание микросервисов Считается, что самыми основными и популярными способами развертывания микросервисов являются следующие пять [1]. Выбор наиболее приемлемого в каждом конкретном случае зависит от потребностей системы. Итак: 1. Один сервер, несколько процессов. 2. Несколько серверов, несколько процессов. Когда мощностей одного сервера уже не хватает, можно просто добавить дополнительные серверы и распределить нагрузку между ними. 3. Контейнеры. Упаковка микросервисов в контейнер упрощает их развертывание и запуск вместе с другими сервисами. Это является первым шагом к оркестровке на основе Kubernetes. 4. Оркестратор. Такие оркестраторы, как Kubernetes или Nomad, представляют собой полноценные платформы, предназначенные для одновременного запуска большого количества контейнеров. 5. Бессерверный. Этот способ позволяет не заморачиваться процессами, контейнерами и серверами, а выполнять код непосредственно в облаке. Таким образом, у нас есть два варианта пути развития: первый — от процессов к контейнерам и в итоге к Kubernetes. И второй — бессерверный. К первому способу (один сервер, несколько процессов) — для наглядности — можно отнести ситуацию, когда вы на своей локальной машине разворачиваете сервер для разработки. При этом вы можете запустить микросервисное приложение в виде нескольких процессов на разных портах. Собственно, так мы и делали до сих пор ранее. Однако разница с производственной средой и локальной разработкой заключается в том, что в первом случае практически всегда используется несколько инстансов (можно сказать, что это уже стандарт), которые следуют за балансировщиком нагрузки. Балансировщик нагрузки контролирует, чтобы ресурсы запущен-


212 Глава 5 ных приложений нагружались равномерно и не было перекоса, когда один инстанс загружен на 80%, а другой, к примеру, только на 20%. Если вы хотите запустить свое приложение на домашнем сервере, вам понадобится получить публичный IP-адрес и открыть соответствующие порты, на которых и будет запущен ваш проект. В качестве положительного момента такого способа можно отметить, что запустить на виртуальной машине (ВМ) один инстанс проекта, моделирующий процессы, выполняющиеся на сервере, не предоставляет особой проблемы. Для этого также не потребуется специально ничего изучать — достаточно подключиться к ВМ по SSH, перенести в нее проект, установить необходимые зависимости и запустить на выполнение. Подробно рассматривать способ реализации этого подхода на практике мы не станем, поскольку он буквально 1:1 повторяет то, как мы устанавливали и настраивали всё это локально, — с той лишь разницей, что тут можно работать только через терминал, а если понадобится отредактировать какой-либо текстовый файл, то делать это придется через встроенные в терминал редакторы vim или nano. Однако когда появляется необходимость масштабировать подобное решение, то начинаются сложности, т. к. добавить в него ресурсы сервера бывает затруднительно, если это решение не облачное. Об отказоустойчивости системы в таком случае можно и не говорить: если ВМ отключить от сети, то приложение перестанет быть доступно. Так что развертывание при каких-либо сбоях может понадобиться делать каждый раз заново собственноручно. При втором способе (несколько серверов, несколько процессов) происходит всё то же самое, что и при первом, с той лишь разницей, что для балансировки и создания общего контекста для инстансов необходимо использовать веб-серверы nginx (HTTP-сервер, который чаще всего выступает в роли прокси-сервера и/или балансировщика нагрузки) или Apache. Чтобы установить nginx на ВМ с предустановленной системой Ubuntu 22.04, необходимо выполнить команду: sudo apt install nginx Проверить, что nginx установился и запустился, можно командой: sudo systemctl status nginx Если он запущен, то будет выведен следующий результат: ● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2023-08-18 18:25:10 UTC; 55s ago Docs: man:nginx(8) Process: 48748 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/S> Process: 48749 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS) Main PID: 48846 (nginx) Tasks: 3 (limit: 4557)


Развертывание микросервисов 213 Memory: 5.3M CPU: 264ms CGroup: /system.slice/nginx.service ├─48846 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;" ├─48849 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" └─48850 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" Aug 18 18:25:10 server systemd[1]: Starting A high performance web server and a reverse proxy server... Aug 18 18:25:10 server systemd[1]: Started A high performance web server and a reverse proxy server. Теперь, если перейти по публичному IP-адресу вашей ВМ, вы увидите, что nginx запущен (рис. 5.1). Рис. 5.1. Nginx запущен При штатной установке nginx будет запущен со своими стандартными настройками. А чтобы запустить его в режиме прокси-сервера, необходимо отредактировать конфигурацию: открыть настройки nginx через консольный редактор: sudo nano /etc/nginx/sites-available/default и подправить их следующим образом (рис. 5.2): location / { proxy_pass http://localhost:9000; } Здесь указывается, куда перенаправлять запрос, который поступает на 80-й порт (по умолчанию открытым под nginx чаще всего оказывается порт 8080 или 80). Однако такой метод настройки развертывания очень неудобный. Чтобы проект запустился, мы должны клонировать весь его код с GitHub, установить и развернуть Docker-Compose для баз данных, установить и запустить каждый сервер по отдельности, а затем еще настроить nginx. К тому же если вы отключитесь от удаленной машины или закроете соединение, где поднимаются сервисы, то сервисы завершат свою работу. Чтобы этого избежать, можно настроить запуск через службы.


214 Глава 5 Рис. 5.2. Редактирование конфигурации nginx В общем что в первом способе, что во втором оказывается слишком много ручной работы. Причем для второго способа, когда задействовано несколько ВМ, настройка nginx будет еще сложнее, поскольку там его ключевая задача — именно балансировка нагрузки. Перед тем как перейти к рассмотрению оставшихся способов развертывания микросервисов, хотелось бы немного поговорить о CI/CD — механизме непрерывной интеграции / непрерывной доставки (Continuous Integration/Continuous Delivery). Механизм CI/CD используется для обеспечения автоматической доставки кода на серверы, где он должен выполняться, для предварительной проверки качества кода, а также и для других, не менее важных процессов, которые необходимо автоматизировать. Для коммерческих проектов на основе механизма CI/CD обычно используют GitLab и внедренный в него GitLab Runner. Впрочем, у GitHub есть и своя альтернатива в виде GitHub Actions. Эти методологии разработки больше относятся уже к практике DevOps и позволяют автоматизировать различные процессы — провер-


Развертывание микросервисов 215 ку того, что все тесты выполняются, что код соответствует всем принятым соглашениям, а само приложение успешно собирается. Также — через настройку CI/CD — производится и выкатка микросервиса на стенды. Мы рассмотрим, как все это устроено на примере GitHub Actions — бесплатного плана для наших целей более чем достаточно, и дополнительные затраты не потребуются. Свои эксперименты мы будем производить над проектом Transaction — для этого понадобится залить его на GitHub. При этом мы подразумеваем, что все необходимые действия с Git уже проделаны, и подключение по SSH активно1 . Для заливки проекта на GitHub выполните специальную команду: git init В открывшемся окне создайте в GitHub репозиторий, назвав его transaction (рис. 5.3). Рис. 5.3. Создание репозитория в GitHub 1 В любом случае сделать это можно по следующей инструкции: https://docs.github.com/ru/authentication/connecting-to-github-with-ssh.


216 Глава 5 Рис. 5.4. Инструкция по подключению к удаленному репозиторию Нажмите кнопку Create repository и пролистайте открывшуюся страницу до самого низа — вы увидите инструкцию по работе с новым репозиторием (рис. 5.4). Нас здесь интересует вторая часть инструкции, которая прописана в разделе «...or push an existing repository from command line» («...или нажмите существующий репозиторий из командной строки»»). Именно это мы и выполним, предварительно закоммитив наши изменения, так что вместо ветки main у нас будет ветка master. В результате весь код окажется на GitHub в созданном только что репозитории (рис. 5.5). Теперь можно переключиться непосредственно в GitHub Actions. Для этого на странице репозитория откройте вкладку Actions. Там приведены различные готовые варианты сценариев непрерывной интеграции, но чтобы лучше в этом разобраться, создадим свой сценарий с нуля сами. Для этого надо перейти по ссылке: set up a workflow yourself (рис. 5.6). GitHub Actions — это платформа непрерывной интеграции и непрерывной доставки (CI/CD), которая позволяет автоматизировать многие процессы, связанные с репозиторием и кодом, который в нем хранится. GitHub Actions использует Workflow — определенный набор инструкций в формате YAML, описывающий последовательность выполнения задач и работающий на основе событий. Например, можно настроить запуск тестов и билд-проекта на


Развертывание микросервисов 225 Обертывание микросервисов в docker-контейнеры Для того чтобы поместить все наши микросервисы в один контейнер, необходимо в первую очередь создать в каждом сервисе файл Dockerfile, в котором будут прописаны инструкции для создания необходимого окружения. Начнем с сервиса Account: Dockerfile FROM node:20.5.1-alpine WORKDIR /account COPY . . COPY .env.example .env RUN npm cache clean --force RUN npm install -g @nestjs/cli argon2 ts-node rimraf RUN npm install RUN yarn build CMD ["yarn", "start:prod" ] Итак, что же здесь происходит? Команда FROM показывает, что будет использоваться в качестве системы. Обратите внимание, что значение может быть как node:latest, так и node:20.5.1. Если установить значение node:latest, то могут возникнуть непредвиденные сложности с совместимостью. Во втором же случае все будет установлено как нужно, однако версия с окончанием на alpine занимает гораздо меньше места, чем ее аналоги. Далее мы устанавливаем каталог account, который будет считаться для нашего контейнера актуальным. Команда COPY . . копирует все содержимое из текущего каталога в каталог account в контейнере. Отдельно мы задаем копирование файла с переменными окружения — поскольку у нас используется везде именно .env, то .env.example нужен здесь только для того, чтобы передавать его и не демонстрировать свои приватные настройки и пароли. Команда RUN npm cache clean –force нужна, чтобы все зависимости установились именно с нуля, а не из кеша.


226 Глава 5 Следующей командой мы устанавливаем глобальные зависимости, которые необходимы для корректной работы нашего приложения: отдельно устанавливаются зависимости именно для проекта, а уже затем выполняется сборка проекта. Команда CMD означает, что это будет выполнено, когда контейнер станут запускать. Команды RUN при этом выполняются в момент создания образа. Чтобы корректно использовать копирование в контейнер, нужно добавить также файл .dockerignore, — он работает по тому же принципу, что и .gitignore: .dockerignore .idea dist node_modules Здесь мы указываем файлы и каталоги, которые не должны попасть в контейнер, тем самым увеличивая его размер. И доработаем также наш файл docker-compose — чтобы из созданного образа формировался сервис наравне с базами данных, и посмотрим, что там поменялось: docker-compose.yaml version: "3.9" services: postgres: image: postgres:latest container_name: book-dbs ports: [ "5132:5432" ] env_file: [ .env.docker ] volumes: - ./docker/volumes/postgres:/var/lib/postgresql/data - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql restart: unless-stopped # Kafka zookeeper: image: confluentinc/cp-zookeeper:latest container_name: book-kafka-zookeeper platform: linux/x86_64 environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_TICK_TIME: 2000 ports: [ "22181:2181" ] restart: unless-stopped kafka: image: confluentinc/cp-kafka:latest


Развертывание микросервисов 227 container_name: book-kafka platform: linux/x86_64 environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://kafka:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 depends_on: [ zookeeper ] ports: [ "29092:29092" ] restart: unless-stopped kafka-ui: image: provectuslabs/kafka-ui:latest container_name: book-kafka-ui platform: linux/x86_64 environment: - KAFKA_CLUSTERS_0_NAME=local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181 depends_on: [ zookeeper, kafka ] ports: [ "29093:8080" ] restart: unless-stopped # Redis redis: container_name: book-redis platform: linux/x86_64 image: docker.io/bitnami/redis:7.0 command: redis-server --requirepass ${REDIS_PASSWORD} ports: - 6379:6379 # networks: # - redis-network env_file: [ .env ] redis-commander: container_name: book-redis-commander image: rediscommander/redis-commander:latest platform: linux/x86_64 restart: always environment: REDIS_HOSTS: redis REDIS_HOST: redis REDIS_PORT: redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD}


228 Глава 5 ports: [ "29094:8081" ] # networks: # - redis-network env_file: [ .env ] account: build: context: . dockerfile: Dockerfile env_file: [ .env ] ports: [ "9000:9000" ] volumes: - ./src:/account/src depends_on: [ postgres, zookeeper, kafka, redis, redis-commander, kafka-ui ] links: - postgres - kafka - redis volumes: redis_data: driver: local Как можно видеть, здесь добавили сборку для account и убрали отдельную сеть для redis — чтобы свободно можно было подключаться из account. В account через поле dockerfile указывается образ, из которого нужно собрать сервис. Так как в качестве основного каталога в dockerfile мы установили account, то и в volumes также необходимо указать, что src находится в каталоге account. Через depends_on устанавливаются зависимости, чтобы сервис запускался после них. Кроме того, поскольку теперь подключения из account к kafka, posgtresql и redis идут изнутри контейнеров, то и хост для подключения также изменился: .env.example ### BASE SERVICE_NAME=account NODE_ENV=local ### HTTP HTTP_PORT=9000 HTTP_HOST=localhost HTTP_PREFIX=/api/account HTTP_VERSION=1 ### DATABASE DB_TYPE=postgres


ПРИЛОЖЕНИЕ Описание электронного архива Электронный архив с примерами к книге выложен на сервер издательства «БХВ» по адресу: https://zip.bhv.ru/9785977519359.zip. Ссылка доступна и со страницы книги на сайте https://bhv.ru/. Структура архива представлена в табл. П1. Таблица П1. Структура электронного архива Каталог, файл Описание account Исходный код микросервиса Account. Инструкции по запуску внутри каталога в файле README.md auth Исходный код микросервиса Auth. Инструкции по запуску внутри каталога в файле README.md transaction Исходный код микросервиса Transaction. Инструкции по запуску внутри каталога в файле README.md local-env Содержит файлы для создания локального окружения local-env/ docker-compose.yaml Файл docker-compose.yaml для создания kafka, kafka-ui, redis, redis-commander, postgresql в контейнерах local-env/sql Содержит SQL-скрипт для создания таблиц local-env/volumes Данные, используемые для postgres Manifest_for_kuber Манифесты для создания подов в Kubernetes. Были сгенерированы из файла docker-compose.yaml через kompose Manifest_for_kuber/ docker-compose Из этого файла docker-compose.yaml были сгенерированы манифесты для kubernetes


Предметный указатель A ACID 151 AMQP 112 Apache Kafka 115 C CI/CD 214 cURL 27 D Docker 25 Docker Hub 232 Dockerfile 225 DTO 57 G git flow 22 GitHub Actions 216 I Insomnia 27 J JWT 84 K kompose 241 Kubernetes 238 N Nest CLI 19 nginx 212 Npm 17 O OSI 103 P Postman 27 protobuf 108 R Redis 117 RESTful API 45 S Saga 185 Swagger UI 27 W Workflow 216 Y YAGNI 161


Click to View FlipBook Version