Русский
Русский
English
Статистика
Реклама

Dao

Как мы в 2020 году изобретали процесс разработки, отладки и доставки в прод изменений базы данных

24.08.2020 14:14:04 | Автор: admin
На дворе 2020 год и фоновым шумом вы уже привыкли слышать: Кубернетес это ответ!, Микросервисы!, Сервис меш!, Сесурити полиси!. Все вокруг бегут в светлое будущее.

Подходы в том, что касается баз данных, в нашей компании более консервативны, чем в прикладных приложениях. Крутится база данных у нас не в кубернетесе, а на железе или в виртуалке. Для изменений базы данных процессинга платежных сервисов у нас есть устоявшийся процесс, который включает в себя множество автоматических проверок, большое ревью и релиз с участием 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.3ADD init.sql /docker-entrypoint-initdb.d/


В init.sql делаем чистую БД, которую рассчитываем получить и на тесте, и в проде. Она должна содержать:

  • Пользователя-владельца схемы и саму схему.
  • Пользователя приложения с грантом на использование схемы.
  • Требуемые EXTENSIONs


Пример init.sql
create role my_awesome_servicewith 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-скрипт в ченджсет. Это гарантирует нам, что гранты доедут до прода.

Пример sql-патчсета
0_ddl.sql:
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;


Пример changeset.yaml
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.

Этот механизм нужен, чтобы катить миграции при старте контекста приложения.
Например:

  1. На тестовой БД патчсеты 001, 002, 003.
  2. Погромист наразрабатывал патчсеты 004, 005 и не деплоил приложение в тест.
  3. Деплоим в тест. Докатываются патчсеты 004, 005.


Если не накатываются приложение не стартует. Rolling update не убивает старые поды.
В нашем стеке JVM + Spring, и мы не используем ORM. Поэтому нам потребовалась интеграция Spring-Liquibase.

У нас в компании есть важное требование безопасности: пользователь приложения должен иметь ограниченный набор грантов и точно не должен иметь доступ уровня владельца схемы. С помощью Spring-Liquibase есть возможность катить миграции от имени пользователя-владельца схемы. При этом пул соединений прикладного уровня приложения не имеет доступа к DataSource'у Liquibase. Поэтому приложение не получит доступ из-под пользователя-владельца схемы.

Пример application-testing.yaml
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 с нужными аргументами.

Пример DAO-теста
@Test@Transactional@Rollbackfun `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:

  1. На билдагенте может быть потенциально занят стандартный порт PostgreSQL 5432 или любой статичный. Мало ли, кто-то не потушил контейнер с базой после завершения тестов.
  2. Из этого вторая задача: нужно тушить контейнер после завершения тестов.

Эти две задачи решает библиотека TestContainers. Она использует существующий докер образ для поднятия контейнера с базой данных в состоянии init.sql.

Пример использования TestContainers
@TestConfigurationpublic 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, по аналогии с микросервисами. Пайплайн является дочерним для микросервиса, которому принадлежит БД.
  • Не храним креды от БД в тимсити. И не заботимся о доступах с виртуалок-билдагентов.


Знаю, что для многих это всё не откровение. Но раз уж вы дочитали, будем рады рассказу о вашем опыте в комментах =)
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru