Подходы в том, что касается баз данных, в нашей компании более консервативны, чем в прикладных приложениях. Крутится база данных у нас не в кубернетесе, а на железе или в виртуалке. Для изменений базы данных процессинга платежных сервисов у нас есть устоявшийся процесс, который включает в себя множество автоматических проверок, большое ревью и релиз с участием DBA. Количество проверок и привлекаемых людей в этом случае негативно влияет на time-to-market. С другой стороны, он отлажен и позволяет надежно вносить изменения в продакшен, минимизируя вероятность что-то сломать. А если что-то сломалось, то нужные люди уже включены в процесс починки. Этот подход делает работу основного сервиса компании стабильнее.
Большинство новых реляционных баз данных для микросервисов мы заводим на PostgreSQL. Отлаженный процесс для Oracle хоть и надёжный, но несет с собой избыточную сложность для маленьких БД. Тащить тяжёлые процессы из прошлого в светлое будущее никто не хочет. Проработкой процесса для светлого будущего заранее никто не занялся. В итоге получили отсутствие стандарта и разножопицу.
Если хотите узнать, к каким проблемам это привело и как мы их порешали, — добро пожаловать под кат.
Проблемы, которые мы решали
Нет единых стандартов версионирования
В лучшем случае это DDL SQL-файлы, которые лежат где-то в директории db в репозитории с микросервисом. Совсем плохо, если это просто текущее состояние БД, разное на тесте и на проде, и эталонных скриптов схемы БД нет.
В ходе отладки ушатываем тестовую базу
«Я сейчас немного тестовую БД пошатаю, не пугайтесь там» — и пошел отлаживать на тестовой базе данных только что написанный код изменения схемы. Иногда долго, и всё это время тестовый контур не работает.
При этом может поломаться тестовый контур в той части, где другие микросервисы взаимодействуют с микросервисом, чью базу ушатал разработчик.
Методы DAO не покрываются тестами, не проверяются в CI
При разработке и отладке методы DAO вызываются через дергание за внешние ручки несколькими слоями выше. Это подвергает проверке целые сценарии бизнес-логики вместо конкретного взаимодействия микросервиса и базы данных.
Гарантии, что ничего не развалится в будущем, нет. Страдает качество и поддерживаемость микросервиса.
Неизоморфность сред
Если в тестовый и продакшен контуры изменения поставляются по-разному, то нельзя быть уверенным, что оно будет работать одинаково. Особенно когда на тесте по факту проводится разработка и отладка.
Объекты на тесте могут быть созданы из-под учетки разработчика или приложения. Гранты накидываются как попало, обычно grant all privileges. Гранты приложению выдаются по принципу «вижу ошибку в логе — даю грант». Часто при релизе забывают про гранты. Иногда после релиза смок-тестирование не покрывает всю новую функциональность и отсутствие гранта выстреливает не сразу.
Тяжелый и ломучий процесс наката в продакшен
Накат в прод сделали ручным, но по аналогии с процессом для Oracle, через согласование DBA, релиз-менеджеров и накат релиз-инженерами.
Это замедляет релиз. А в случае проблем увеличивает даунтайм, усложняя доступ разработчика к БД. Скрипты exec.sql и rollback.sql часто не проверялись на тесте, потому что стандарта патчсетирования для не-Oracle нет, а на тест катилось как попало.
Поэтому бывает такое, что в некритичные сервисы разработчики катят изменения без этого процесса вообще.
Как можно делать, чтобы было хорошо
Отладка на локальной БД в докер-контейнере
Для кого-то могут показаться очевидными вообще все технические решения, описанные в статье. Но почему-то из года в год я вижу людей, которые с энтузиазмом наступают на одни и те же грабли.
Вот вы же не лезете на тестовый сервер по ssh, чтобы писать и дебажить код приложения? Я считаю, что разрабатывать и отлаживать код базы данных на тестовом инстансе БД — так же абсурдно. Есть исключения, бывает, что поднять локально базу данных очень сложно. Но обычно, если мы говорим о чем-то легковесном и не-легаси, то поднять локально базу и накатить на нее последовательно все миграции не составляет большого труда. Взамен вы получите стабильный инстанс под боком, который не ушатает другой разработчик, до которого не пропадут доступы и на котором вы имеете нужные для разработки права.
Приведу пример, насколько просто поднять локально БД:
Пишем двухстрочный Dockerfile:
FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/
В init.sql делаем «чистую» БД, которую рассчитываем получить и на тесте, и в проде. Она должна содержать:
- Пользователя-владельца схемы и саму схему.
- Пользователя приложения с грантом на использование схемы.
- Требуемые EXTENSIONs
create role my_awesome_service
with login password *** NOSUPERUSER inherit CREATEDB CREATEROLE NOREPLICATION;
create tablespace my_awesome_service owner my_awesome_service location '/u01/postgres/my_awesome_service_data';
create schema my_awesome_service authorization my_awesome_service;
grant all on schema my_awesome_service to my_awesome_service;
grant usage on schema my_awesome_service to my_awesome_service;
alter role my_awesome_service set search_path to my_awesome_service,pg_catalog, public;
create user my_awesome_service_app with LOGIN password *** NOSUPERUSER inherit NOREPLICATION;
grant usage on schema my_awesome_service to my_awesome_service_app;
create extension if not exists "uuid-ossp";
Для удобства можно добавить в Makefile таску db, которая (пере)запустит контейнер с базой и оттопырит порт для соединения:
db:
docker container rm -f my_awesome_service_db || true
docker build -t my_awesome_service_db docker/db/.
docker run -d --name my_awesome_service_db -p 5433:5432 my_awesome_service_db
Версионирование changeset‘ов с помощью чего-то стандартного для индустрии
Тоже выглядит очевидно: нужно писать миграции и содержать их в системе контроля версий. Но очень часто я вижу «голые» sql-скрипты, без какой-либо обвязки. И это значит, что нет никакого контроля наката и отката, кем, что и когда было накачено. Нет даже гарантии, что ваши SQL-скрипты могут быть выполнены на тестовой и продовой БД, так как ее структура могла измениться.
В общем, нужен контроль. Системы миграции как раз про контроль.
Не будем вдаваться в сравнение разных систем версионирования схем БД. FlyWay vs Liquibase — не тема этой статьи. Мы выбрали Liquibase.
Мы версионируем:
- DDL-структуру объектов бд (create table).
- DML-содержимое таблиц-справочников (insert, update).
- DCL-гранты для УЗ Приложения (grant select, insert on ...).
Запуская и отлаживая микросервис на локальной БД, разработчик столкнется с необходимостью позаботиться о грантах. Единственный легальный способ для него — завести DCL-скрипт в ченджсет. Это гарантирует нам, что гранты доедут до прода.
create table my_awesome_service.ref_customer_type
(
customer_type_code varchar not null,
customer_type_description varchar not null,
constraint ref_customer_type_pk primary key (customer_type_code)
);
alter table my_awesome_service.ref_customer_type
add constraint customer_type_code_ck check ( (customer_type_code)::text = upper((customer_type_code)::text) );
1_dcl.sql:
grant select on all tables in schema my_awesome_service to ru_svc_qw_my_awesome_service_app;
grant insert, update on my_awesome_service.some_entity to ru_svc_qw_my_awesome_service_app;
2_dml_refs.sql:
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('INDIVIDUAL', 'Физ. лицо');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('LEGAL_ENTITY', 'Юр. лицо');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('FOREIGN_AGENCY', 'Иностранное юр. лицо');
Fixtures. Данные для тестов или отладки идут отдельным ченжсетом с контекстом dev
3_dml_dev.sql:
insert into my_awesome_service.some_entity_state (state_type_code, state_data, some_entity_id)
values ('BINDING_IN_PROGRESS', '{}', 1);
rollback.sql:
drop table my_awesome_service.ref_customer_type;
databaseChangeLog:
- changeSet:
id: 1
author: "mr.awesome"
changes:
- sqlFile:
path: db/changesets/001_init/0_ddl.sql
- sqlFile:
path: db/changesets/001_init/1_dcl.sql
- sqlFile:
path: db/changesets/001_init/2_dml_refs.sql
rollback:
sqlFile:
path: db/changesets/001_init/rollback.sql
- changeSet:
id: 2
author: "mr.awesome"
context: dev
changes:
- sqlFile:
path: db/changesets/001_init/3_dml_dev.sql
Liquibase создает на БД таблицу databasechangelog, где отмечает накаченные ченджсеты.
Автоматически вычисляет, сколько ченджсетов нужно докатить до БД.
Есть maven и gradle plugin с возможностью сгенерировать из нескольких ченджсетов скрипт, который нужно докатить до БД.
Интеграция системы миграций БД в фазу запуска приложения
Здесь мог бы быть любой адаптер системы контроля миграций и фреймворка, на котором построено ваше приложение. Со многими фреймворками он идёт в комплекте с ORM. Например, Ruby-On-Rails, Yii2, Nest.JS.
Этот механизм нужен, чтобы катить миграции при старте контекста приложения.
Например:
- На тестовой БД патчсеты 001, 002, 003.
- Погромист наразрабатывал патчсеты 004, 005 и не деплоил приложение в тест.
- Деплоим в тест. Докатываются патчсеты 004, 005.
Если не накатываются — приложение не стартует. Rolling update не убивает старые поды.
В нашем стеке JVM + Spring, и мы не используем ORM. Поэтому нам потребовалась интеграция Spring-Liquibase.
У нас в компании есть важное требование безопасности: пользователь приложения должен иметь ограниченный набор грантов и точно не должен иметь доступ уровня владельца схемы. С помощью Spring-Liquibase есть возможность катить миграции от имени пользователя-владельца схемы. При этом пул соединений прикладного уровня приложения не имеет доступа к DataSource'у Liquibase. Поэтому приложение не получит доступ из-под пользователя-владельца схемы.
spring:
liquibase:
enabled: true
database-change-log-lock-table: "databasechangeloglock"
database-change-log-table: "databasechangelog"
user: ${secret.liquibase.user:}
password: ${secret.liquibase.password:}
url: "jdbc:postgresql://my.test.db:5432/my_awesome_service?currentSchema=my_awesome_service"
DAO тесты на CI-этапе verify
В нашей компании есть такой CI-этап — verify. На этом этапе происходит проверка изменений на соответствие внутренним стандартам качества. Для микросервисов это обычно прогон линтера для проверки кодстайла и на наличие багов, прогон unit-тестов и запуск приложения с поднятием контекста. Теперь на этапе verify можно проверить миграции БД и взаимодействие DAO-слоя приложения с БД.
Поднятие контейнера с БД и накат патчсетов увеличивает время старта Spring-контекста на 1,5-10 сек, в зависимости от мощности рабочей машины и количества патчсетов.
Это не совсем unit-тесты, это тесты интеграции DAO-слоя приложения с базой данных.
Называя БД частью микросервиса, мы говорим, что это тестирование интеграции двух частей одного микросервиса. Без внешних зависимостей. Таким образом эти тесты стабильны и могут выполняться на этапе verify. Они фиксируют контракт микросервиса и БД, обеспечивая уверенность при будущих доработках.
А еще это удобный способ отладки DAO. Вместо того, чтобы вызывать RestController, имитируя поведения пользователя в каком-то бизнес-сценарии, сразу вызываем DAO с нужными аргументами.
@Test
@Transactional
@Rollback
fun `create cheque positive flow`() {
jdbcTemplate.update(
"insert into my_awesome_service.some_entity(inn, registration_source_code)" +
"values (:inn, 'QIWICOM') returning some_entity_id",
MapSqlParameterSource().addValue("inn", "526317984689")
)
val insertedCheque = chequeDao.addCheque(cheque)
val resultCheque = jdbcTemplate.queryForObject(
"select cheque_id from my_awesome_service.cheque " +
"order by cheque_id desc limit 1", MapSqlParameterSource(), Long::class.java
)
Assert.assertTrue(insertedCheque.isRight())
Assert.assertEquals(insertedCheque, Right(resultCheque))
}
Есть две сопутствующие задачи для прогона этих тестов в пайплайне на verify:
- На билдагенте может быть потенциально занят стандартный порт PostgreSQL 5432 или любой статичный. Мало ли, кто-то не потушил контейнер с базой после завершения тестов.
- Из этого вторая задача: нужно тушить контейнер после завершения тестов.
Эти две задачи решает библиотека TestContainers. Она использует существующий докер образ для поднятия контейнера с базой данных в состоянии init.sql.
@TestConfiguration
public class DatabaseConfiguration {
@Bean
GenericContainer postgreSQLContainer() {
GenericContainer container = new GenericContainer("my_awesome_service_db")
.withExposedPorts(5432);
container.start();
return container;
}
@Bean
@Primary
public DataSource onlineDbPoolDataSource(GenericContainer postgreSQLContainer) {
return DataSourceBuilder.create()
.driverClassName("org.postgresql.Driver")
.url("jdbc:postgresql://localhost:"
+ postgreSQLContainer.getMappedPort(5432)
+ "/postgres")
.username("my_awesome_service_app")
.password("my_awesome_service_app_pwd")
.build();
}
@Bean
@LiquibaseDataSource
public DataSource liquibaseDataSource(GenericContainer postgreSQLContainer) {
return DataSourceBuilder.create()
.driverClassName("org.postgresql.Driver")
.url("jdbc:postgresql://localhost:"
+ postgreSQLContainer.getMappedPort(5432)
+ "/postgres")
.username("my_awesome_service")
.password("my_awesome_service_app_pwd")
.build();
}
С разработкой и отладкой разобрались. Теперь нужно доставить изменения схемы БД в продакшен.
Kubernetes — это ответ! А какой был ваш вопрос?
Итак, вам надо автоматизировать какой-то CI/CD-процесс. У нас есть обкатанный подход на тимсити. Казалось бы, где тут повод для еще одной статьи?
А повод есть. Кроме обкатанного подхода, есть и поднадоевшие проблемки большой компании.
- Билдагентов тимсити на всех не хватает.
- Лицензия стоит денег.
- Настройки виртуалок билдагентов делаются по старинке, через репозитории с конфигами и puppet.
- Доступы с билдагентов до целевых сетей пропиливать надо по старинке.
- Логины-пароли для наката изменений на базу тоже хранятся по старинке.
И во всем этом «по старинке» проблема — все бегут в светлое будущее, а поддержка легаси… ну вы знаете. Работает и ладно. Не работает — займемся потом. Когда-нибудь. Не сегодня.
Допустим, вы уже одной ногой по колено в светлом будущем и кубернетес-инфраструктура у вас уже есть. Есть даже возможность сгенерировать еще один микросервис, который сразу заведется в этой инфраструктуре, подхватит нужный конфиг и секреты, будет иметь нужные доступы, зарегистрируется в service mesh инфраструктуре. И всё это счастье может получить рядовой разработчик, без привлечения человека с ролью *OPS. Вспоминаем, что в кубернетесе есть тип ворклоада Job, как раз предназначенный для каких-то сервисных работ. Ну и погнали делать приложение на Kotlin+Spring-Liquibase, стараясь максимально переиспользовать существующую в компании инфраструктуру для микросервисов на JVM в кубере.
Переиспользуем следующие аспекты:
- Генерация проекта.
- Деплой.
- Доставку конфигов и секретов.
- Доступы.
- Логирование и доставка логов в ELK.
Получаем такой пайплайн:
Кликабельно
Теперь мы имеем
- Версионирование ченджсетов.
- Проверяем их на выполнимость update > rollback.
- Пишем тесты на DAO. Бывает даже следуем TDD: запускаем отладку DAO с помощью тестов. Тесты выполняются на свежеподнятой БД в TestContainers.
- Запускаем локально БД в докере на стандартном порту. Проводим отладку, смотрим, что осталось в БД. При необходимости можем управлять локальной БД вручную.
- Накатываем в тест и проводим авторелиз патчсетов стандартным пайплайном в teamcity, по аналогии с микросервисами. Пайплайн является дочерним для микросервиса, которому принадлежит БД.
- Не храним креды от БД в тимсити. И не заботимся о доступах с виртуалок-билдагентов.
Знаю, что для многих это всё не откровение. Но раз уж вы дочитали, будем рады рассказу о вашем опыте в комментах.
granvi
Так что делать с легаси ораклиной?
Микросервисы это всё понятно. Особенно новые.
А как быть с полсотней разработчиков разной степени криворукости, которым ушатывать тестовую бд. Но не простую, а тестовый контур сверхбрльшой БД? Которая завязана кучей щупалец с другими сервисами?
Как взять ее под контроль версий?
nkh Автор
Выглядит как запрос на отдельную статью о том, как мы решали аналогичную боль =)
Напишем статью, если это и правда востребовано. Ставьте upvote, если откликнулось.
Если коротко, то это процесс + техническое решение, основная цель которых — контроль и проверки на всех этапах.
SubSerman
Было бы интересно почитать
orthoxerox
Так же. С какого-то момента все изменения схемы — ченжсетами в любом формате. Перед продом стоит препрод, на который нельзя накатить что-то руками. На нём все update+rollback прогоняются вашим CI/CD перед мёржем, после чего делается флэшбэк (если это ораклина), а после мёржа делается update. Для чистоты совести можете обновлять его по выходным.
zinaimaz
Много лет назад читал на Хабре про железяку с помощью которой версии БД делаются снэпшотами файловой системы. В результате выходит довольно экономично для огромных тестовых БД. А сейчас это и программно доступно, снэпшоты в файловых системах типа ZFS и др.