Nossa empresa tem uma abordagem mais conservadora quando se trata de bancos de dados do que de aplicativos. O banco de dados está girando não no Kubernetes, mas no hardware ou em uma máquina virtual. Temos um processo bem estabelecido para alterações na base de dados de processamento de pagamentos, que inclui muitos cheques automatizados, uma grande revisão e uma liberação com a participação do DBA. O número de cheques e pessoas envolvidas neste caso afeta negativamente o tempo de colocação no mercado. Por outro lado, ele é depurado e permite que você faça alterações na produção de forma confiável, minimizando as chances de quebrar algo. E se algo quebrar, as pessoas certas já estão incluídas no processo de reparo. Esta abordagem torna o trabalho do serviço principal da empresa mais estável.
Iniciamos a maioria dos novos bancos de dados relacionais para microsserviços no PostgreSQL. Um processo ajustado para Oracle, embora robusto, carrega consigo complexidade desnecessária para bancos de dados pequenos. Ninguém quer arrastar processos difíceis do passado para um futuro brilhante. Ninguém começou a trabalhar no processo para um futuro brilhante com antecedência. Como resultado, ficamos sem um padrão e raznozhopitsu. Se você quiser saber a que problemas isso causou e como os resolvemos, seja bem-vindo ao gato.

Problemas que resolvemos
Não há padrões uniformes para controle de versão
Na melhor das hipóteses, são arquivos DDL SQL que estão localizados em algum lugar no diretório db no repositório com o microsserviço. É muito ruim se este for apenas o estado atual do banco de dados, diferente no teste e na produção, e não houver scripts de referência para o esquema do banco de dados.
Durante a depuração, quebramos a base de teste
“Estou sacudindo um pouco o banco de dados de teste agora, não se assuste com isso” - e fui depurar o código de mudança de esquema recém-escrito no banco de dados de teste. Às vezes leva muito tempo, e todo esse tempo o circuito de teste não funciona.
Ao mesmo tempo, o circuito de teste pode quebrar na parte onde outros microsserviços interagem com o microsserviço, cuja base o desenvolvedor destruiu.
Métodos DAO não são cobertos por testes, não são validados em CI
Ao desenvolver e depurar, os métodos DAO são chamados puxando as alças externas algumas camadas acima. Isso expõe cenários inteiros de lógica de negócios em vez de interações específicas entre o microsserviço e o banco de dados.
Não há garantia de que nada vai desmoronar no futuro. A qualidade e a manutenção do microsserviço são prejudicadas.
Não isomorfismo da mídia
Se os loops de mudança forem entregues de maneira diferente para teste e produção, você não pode ter certeza de que funcionará da mesma forma. Especialmente quando o desenvolvimento e a depuração são realmente executados no teste.
Os objetos no teste podem ser criados sob a conta do desenvolvedor ou aplicativo. As concessões são entregues aleatoriamente, geralmente concedem todos os privilégios. As bolsas são atribuídas ao aplicativo com base no princípio “Vejo um erro no registro - eu dou uma bolsa”. As concessões são freqüentemente esquecidas no lançamento. Às vezes, após o lançamento, o teste de fumaça não cobre todas as novas funcionalidades e a falta de uma concessão não é acionada imediatamente.
Processo pesado e frágil de laminação para a produção
A entrada em produção foi feita manualmente, mas por analogia com o processo da Oracle, por meio da aprovação do DBA, dos gerentes de versão e da implementação pelos engenheiros de versão.
Isso retarda o lançamento. E em caso de problemas, aumenta o tempo de inatividade, dificultando o acesso do desenvolvedor ao banco de dados. Os scripts exec.sql e rollback.sql geralmente não foram testados no teste, porque não existe um padrão de patchsetting para não Oracle e o teste estava indo até o fim.
Portanto, acontece que os desenvolvedores implementam alterações em serviços não críticos sem esse processo.
Como você pode fazer para ser bom
Depuração em um banco de dados local em um contêiner docker
Para alguns, todas as soluções técnicas descritas no artigo podem parecer óbvias. Mas por alguma razão, ano após ano, vejo pessoas que entusiasticamente pisam no mesmo ancinho.
Você não vai para o servidor de teste via ssh para escrever e depurar o código do aplicativo, vai? Acho tão absurdo desenvolver e depurar código de banco de dados em uma Instância de banco de dados de teste. Existem exceções, acontece que é muito difícil aumentar o banco de dados localmente. Mas normalmente, se estamos falando sobre algo leve e não legado, então não é difícil aumentar a base localmente e rolar todas as migrações consistentemente. Em troca, você receberá uma instância estável ao seu lado, que não está atolada por outro desenvolvedor, à qual o acesso não será perdido e na qual você tem os direitos necessários para o desenvolvimento.
Aqui está um exemplo de como é fácil criar um banco de dados local:
Vamos escrever um Dockerfile de duas linhas:
FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/
Em init.sql, fazemos um banco de dados "limpo", que esperamos obter tanto no teste quanto na produção. Deve conter:
- O proprietário do esquema e o próprio esquema.
- Usuário do aplicativo com permissão para usar o esquema.
- EXTENSÕES necessárias
Exemplo de Init.sql
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";
Por conveniência, você pode adicionar a tarefa db ao Makefile, que irá (re) iniciar o contêiner com a base e projetar a porta para conexão:
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
Controle de versão de conjuntos de mudanças com algo padrão da indústria
Também parece óbvio: você precisa escrever migrações e mantê-las no sistema de controle de versão. Mas muitas vezes vejo scripts sql "nus", sem qualquer ligação. E isso significa que não há controle sobre rollback e rollback, por quem, o quê e quando foi bombeado. Não há nem mesmo uma garantia de que seus scripts SQL possam ser executados no banco de dados de teste e produção, pois sua estrutura pode ter sido alterada.
Em geral, você precisa de controle. Os sistemas de migração são apenas sobre controle.
Não faremos uma comparação de diferentes sistemas de controle de versão de esquema de banco de dados. FlyWay vs Liquibase não é o assunto deste artigo. Escolhemos Liquibase.
Nós versão:
- Estrutura DDL de objetos de banco de dados (criar tabela).
- Conteúdo DML das tabelas de pesquisa (inserir, atualizar).
- Concessões DCL para Aplicações UZ (selecionar concessão, inserir em ...).
Ao iniciar e depurar um microsserviço em um banco de dados local, o desenvolvedor se depara com a necessidade de cuidar das concessões. A única maneira legal para isso é adicionar um script DCL ao changeset. Isso garante que as bolsas serão vendidas.
Exemplo de patchset Sql
0_ddl.sql:
1_dcl.sql:
2_dml_refs.sql:
Fixtures. dev
3_dml_dev.sql:
rollback.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;
Exemplo de 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 cria uma tabela databasechangelog no banco de dados, onde anota os changesets aumentados.
Calcula automaticamente quantos conjuntos de alterações você precisa para transferir para o banco de dados.
Há um plug-in maven e um gradle com a capacidade de gerar um script a partir de vários conjuntos de alterações que precisam ser inseridos no banco de dados.
Integração do sistema de migração de banco de dados na fase de lançamento do aplicativo
Pode ser qualquer adaptador do sistema de controle de migração e a estrutura na qual seu aplicativo é construído. Com muitos frameworks, ele vem com o ORM. Por exemplo, Ruby-On-Rails, Yii2, Nest.JS.
Este mecanismo é necessário para rolar migrações quando o contexto do aplicativo é iniciado.
Por exemplo:
- No banco de dados de teste, patchsets 001, 002, 003.
- O pogromist desenvolveu patchsets 004, 005 e não implantou o aplicativo para o teste.
- Implante para o teste. Os patchsets 004, 005 estão sendo lançados.
Se eles não rolarem, o aplicativo não será iniciado. A atualização contínua não mata pods antigos.
Nossa pilha é JVM + Spring e não estamos usando ORM. Portanto, precisávamos da integração Spring-Liquibase .
Temos um requisito de segurança importante em nossa empresa: o usuário do aplicativo deve ter um conjunto limitado de concessões e, definitivamente, não deve ter acesso de nível de proprietário do esquema. Com o Spring-Liquibase, é possível fazer migrações em nome do usuário proprietário do esquema. Nesse caso, o pool de conexão do nível de aplicativo do aplicativo não tem acesso ao DataSource Liquibase. Portanto, o aplicativo não obterá acesso do usuário proprietário do esquema.
Exemplo de 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"
Os testes DAO no estágio de CI verificam
Nossa empresa tem um estágio de CI - verifique. Nesta fase, as alterações são verificadas quanto ao cumprimento dos padrões internos de qualidade. Para microsserviços, geralmente é uma execução de linter para verificar o estilo do código e para bugs, uma execução de teste de unidade e um lançamento de aplicativo com levantamento de contexto. Agora, na etapa de verificação, você pode verificar as migrações do banco de dados e a interação da camada DAO do aplicativo com o banco de dados.
Aumentar um contêiner com um banco de dados e conjuntos de patches contínuos aumenta o tempo de início do contexto Spring em 1,5 a 10 segundos, dependendo da potência da máquina de trabalho e do número de conjuntos de patches.
Esses não são realmente testes de unidade, são testes para integrar a camada DAO do aplicativo com o banco de dados.
Ao chamar um banco de dados de parte de um microsserviço, dizemos que ele está testando a integração de duas partes de um microsserviço. Sem dependências externas. Portanto, esses testes são estáveis e podem ser executados durante a fase de verificação. Eles corrigem o contrato de microsserviço e banco de dados, fornecendo garantia para melhorias futuras.
Também é uma maneira prática de depurar DAOs. Em vez de chamar RestController, simulando o comportamento do usuário em algum cenário de negócios, chamamos imediatamente o DAO com os argumentos necessários.
Exemplo de teste 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))
}
Existem duas tarefas relacionadas para executar esses testes no pipeline de verificação:
- O agente de construção pode estar ocupado com a porta 5432 padrão do PostgreSQL ou com qualquer porta estática. Nunca se sabe, alguém não colocou o recipiente com a base depois que os testes foram concluídos.
- A partir desta, a segunda tarefa: você precisa extinguir o recipiente após a conclusão dos testes.
A biblioteca TestContainers resolve essas duas tarefas . Ele usa uma imagem docker existente para abrir o contêiner de banco de dados no estado init.sql.
Exemplo de uso de TestContainers
@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();
}
Com o desenvolvimento e a depuração resolvidos. Agora precisamos entregar as mudanças do esquema do banco de dados para produção.
Kubernetes é a resposta! Qual foi a sua pergunta?
Portanto, você precisa automatizar alguns processos de CI / CD. Temos uma abordagem de cidade de equipe testada e comprovada. Ao que parece, onde está a razão para outro artigo?
E há uma razão. Além da abordagem testada e comprovada, também existem problemas enfadonhos de uma grande empresa.
- Não há construtores de cidades em equipe suficientes para todos.
- Uma licença custa dinheiro.
- As configurações das máquinas virtuais buildagent são feitas à moda antiga, através dos repositórios com configs e fantoches.
- O acesso dos construtores às redes alvo deve ser feito à moda antiga.
- Logins e senhas para avançar as alterações no banco de dados também são armazenados da maneira antiga.
E com tudo isso "à moda antiga", o problema é - todos estão correndo para um futuro brilhante, e o suporte do Legacy ... você sabe. Funciona e está bem. Não funciona - lidaremos com isso mais tarde. Algum dia. Hoje nao.
Digamos que você já tenha uma perna até o joelho em um futuro brilhante e já tenha uma infraestrutura do Kubernetes. Existe ainda a oportunidade de gerar outro microsserviço, que irá iniciar imediatamente nesta infraestrutura, coletar a configuração e os segredos necessários, ter o acesso necessário e se registrar na infraestrutura de malha de serviço. E toda essa felicidade pode ser obtida por um desenvolvedor comum, sem envolver uma pessoa com a função de * OPS. Lembramos que no Kubernetes existe um tipo de carga de trabalho de Job, destinada apenas a algum tipo de serviço. Pois bem, partimos para fazer uma aplicação em Kotlin + Spring-Liquibase, tentando reaproveitar ao máximo a infraestrutura existente na empresa para microsserviços em JVM em kubera.
Vamos reutilizar os seguintes aspectos:
- Geração do projeto.
- Implante.
- Entrega de configurações e segredos.
- Acesso.
- Registro e entrega de logs para ELK.
Conseguimos um pipeline : clicável

Agora temos
- Controle de versão do changeset.
- Nós os verificamos para atualização de viabilidade → reversão.
- Escrevendo testes para DAO. Às vezes, até seguimos o TDD: executamos a depuração DAO usando testes. Os testes são executados em um banco de dados recém-criado em TestContainers.
- Execute o banco de dados docker localmente em uma porta padrão. Estamos depurando, olhando o que resta no banco de dados. Se necessário, podemos gerenciar o banco de dados local manualmente.
- Passamos para os conjuntos de patches de teste e lançamento automático com um pipeline padrão em teamcity, por analogia com microsserviços. O pipeline é filho do microsserviço que possui o banco de dados.
- Não armazenamos créditos do banco de dados na cidade da equipe. E não nos importamos com acessos de construtores virtuais.
Eu sei que para muitos isso não é uma revelação. Mas já que você acabou de ler, teremos o maior prazer em compartilhar sua experiência nos comentários.