Cómo reinventamos en 2020 el proceso de desarrollo, depuración y entrega de cambios de base de datos a prod

Es 2020 en el patio y ya estás acostumbrado a escuchar con ruido de fondo: "¡Kubernetes es la respuesta!", "¡Microservicios!", "¡Malla de servicio!", "¡Políticas de Sesuriti!" Todo el mundo corre hacia un futuro brillante.



Nuestra empresa tiene un enfoque más conservador cuando se trata de bases de datos que de aplicaciones. La base de datos no gira en Kubernetes, sino en hardware o en una máquina virtual. Tenemos un proceso bien establecido para cambiar la base de datos de procesamiento de pagos, que incluye muchas verificaciones automatizadas, una revisión amplia y una publicación con la participación del DBA. El número de controles y personas involucradas en este caso afecta negativamente el time-to-market. Por otro lado, está depurado y le permite realizar cambios confiables en la producción, minimizando la probabilidad de romper algo. Y si algo se rompe, las personas adecuadas ya están incluidas en el proceso de reparación. Este enfoque hace que el trabajo del servicio principal de la empresa sea más estable.



Iniciamos la mayoría de las nuevas bases de datos relacionales para microservicios en PostgreSQL. Un proceso ajustado para Oracle, aunque robusto, conlleva una complejidad innecesaria para las bases de datos pequeñas. Nadie quiere arrastrar procesos difíciles del pasado a un futuro brillante. Nadie empezó a trabajar en el proceso para un futuro brillante por adelantado. Como resultado, nos faltó un estándar y raznozhopitsu. Si quieres saber qué problemas ocasionó esto y cómo los resolvimos, bienvenido a cat.











Problemas que resolvimos



No hay estándares de versiones uniformes



En el mejor de los casos, estos son archivos SQL DDL que se encuentran en algún lugar del directorio db en el repositorio con el microservicio. Es muy malo si este es solo el estado actual de la base de datos, diferente en la prueba y en la producción, y no hay scripts de referencia para el esquema de la base de datos.



Durante la depuración, destruimos la base de pruebas



"Estoy agitando un poco la base de datos de prueba ahora, no se alarme", y fui a depurar el código de cambio de esquema recién escrito en la base de datos de prueba. A veces lleva mucho tiempo y durante todo este tiempo el circuito de prueba no funciona.



Al mismo tiempo, el circuito de prueba puede romperse en la parte donde otros microservicios interactúan con el microservicio, cuya base ha arruinado el desarrollador.



Los métodos DAO no están cubiertos por pruebas, no están validados en CI



Al desarrollar y depurar, los métodos DAO se invocan tirando de los identificadores externos unas pocas capas arriba. Esto expone escenarios completos de lógica empresarial en lugar de interacciones específicas entre el microservicio y la base de datos.



No hay garantía de que nada se derrumbe en el futuro. La calidad y la capacidad de mantenimiento del microservicio sufren.



No isomorfismo de los medios



Si los bucles de cambio se entregan de manera diferente para prueba y producción, entonces no puede estar seguro de que funcionará de la misma manera. Especialmente cuando el desarrollo y la depuración se llevan a cabo en la prueba.



Los objetos de la prueba se pueden crear bajo la cuenta del desarrollador o la aplicación. Las subvenciones se entregan al azar, generalmente otorgan todos los privilegios. Las subvenciones para la solicitud se emiten según el principio "Veo un error en el registro, doy una subvención". Las subvenciones a menudo se olvidan en el momento de su publicación. A veces, después del lanzamiento, las pruebas de humo no cubren todas las funciones nuevas y la falta de una subvención no se activa de inmediato.



Proceso pesado y frágil de puesta en producción



La puesta en producción se realizó manualmente, pero por analogía con el proceso de Oracle, mediante la aprobación del DBA, los administradores de versiones y la puesta en marcha por los ingenieros de versiones.



Esto ralentiza el lanzamiento. Y en caso de problemas, aumenta el tiempo de inactividad, lo que complica el acceso del desarrollador a la base de datos. Los scripts exec.sql y rollback.sql a menudo no se probaron en la prueba, porque no existe un estándar de configuración de parches para los que no son de Oracle, y la prueba fue completa.



Por lo tanto, sucede que los desarrolladores implementan cambios en servicios no críticos sin este proceso en absoluto.



¿Cómo puedes hacer para ser bueno?



Depurar en una base de datos local en un contenedor docker



Para algunos, todas las soluciones técnicas descritas en el artículo pueden parecer obvias. Pero por alguna razón, de año en año, veo personas que pisan con entusiasmo el mismo rastrillo.



No va al servidor de prueba a través de ssh para escribir y depurar el código de la aplicación, ¿verdad? Me parece igualmente absurdo desarrollar y depurar código de base de datos en una instancia de base de datos de prueba. Hay excepciones, sucede que es muy difícil levantar la base de datos localmente. Pero, por lo general, si hablamos de algo ligero y no heredado, entonces no es difícil aumentar la base localmente y aplicar todas las migraciones de manera consistente. A cambio, obtendrás una instancia estable a tu lado, que no se verá empantanada por otro desarrollador, a la que no perderás acceso y sobre la que tienes los derechos necesarios para el desarrollo.



Aquí hay un ejemplo de lo fácil que es abrir una base de datos local:



Escribamos un Dockerfile de dos líneas:



FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/


En init.sql creamos una base de datos "limpia", que esperamos obtener tanto en la prueba como en producción. Debe contener:



  • El propietario del esquema y el propio esquema.
  • Usuario de la aplicación con autorización para utilizar el esquema.
  • EXTENSIONES requeridas


Ejemplo 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";




Para su comodidad, puede agregar la tarea db al Makefile, que (re) iniciará el contenedor con la base y sobresaldrá el puerto para la conexión:



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


Control de versiones de conjuntos de cambios con algo estándar de la industria



También parece obvio: debe escribir migraciones y mantenerlas en el sistema de control de versiones. Pero muy a menudo veo scripts sql "desnudos", sin ningún tipo de enlace. Y esto significa que no hay control sobre la reversión y la reversión, por quién, qué y cuándo se bombeó. Ni siquiera hay garantía de que sus scripts SQL se puedan ejecutar en la base de datos de prueba y producción, ya que su estructura puede haber cambiado.



En general, necesitas control. Los sistemas de migración solo tienen que ver con el control.

No entraremos en una comparación de diferentes sistemas de control de versiones de esquemas de bases de datos. FlyWay vs Liquibase no es el tema de este artículo. Elegimos Liquibase.



Nosotros versionamos:



  • Estructura DDL de objetos de base de datos (crear tabla).
  • Contenido DML de tablas de búsqueda (insertar, actualizar).
  • Subvenciones DCL para aplicaciones UZ (seleccionar subvención, insertar en ...).


Al lanzar y depurar un microservicio en una base de datos local, el desarrollador se enfrentará a la necesidad de hacerse cargo de las subvenciones. La única forma legal de hacerlo es agregar un script DCL al conjunto de cambios. Esto asegura que las subvenciones se pondrán a la venta.



Ejemplo de conjunto de parches 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;




Ejemplo 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 crea una tabla de registro de cambios de base de datos en la base de datos, donde toma nota de los conjuntos de cambios bombeados.

Calcula automáticamente cuántos conjuntos de cambios necesita para transferir a la base de datos.



Hay un maven y un complemento de gradle con la capacidad de generar un script a partir de varios conjuntos de cambios que deben incorporarse a la base de datos.



Integración del sistema de migración de la base de datos en la fase de lanzamiento de la aplicación



Podría ser cualquier adaptador del sistema de control de migración y el marco en el que se construye su aplicación. Con muchos marcos, viene incluido con el ORM. Por ejemplo, Ruby-On-Rails, Yii2, Nest.JS.



Este mecanismo es necesario para realizar migraciones cuando se inicia el contexto de la aplicación.

Por ejemplo:



  1. En la base de datos de prueba, conjuntos de parches 001, 002, 003.
  2. El pogromista desarrolló los conjuntos de parches 004, 005 y no implementó la aplicación para la prueba.
  3. Implementar para la prueba. Se están implementando los conjuntos de parches 004, 005.


Si no ruedan, la aplicación no se inicia. La actualización continua no mata los pods antiguos.



Nuestra pila es JVM + Spring y no estamos usando ORM. Por lo tanto, necesitábamos la integración Spring-Liquibase .



Tenemos un requisito de seguridad importante en nuestra empresa: el usuario de la aplicación debe tener un conjunto limitado de concesiones y definitivamente no debe tener acceso a nivel de propietario de esquema. Con Spring-Liquibase es posible realizar migraciones en nombre del usuario propietario del esquema. En este caso, el grupo de conexiones del nivel de aplicación de la aplicación no tiene acceso a Liquibase DataSource. Por lo tanto, la aplicación no obtendrá acceso del usuario propietario del esquema.



Ejemplo 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"




Las pruebas DAO en la etapa CI verifican



Nuestra empresa tiene una etapa de CI de este tipo: verifique. En esta etapa, se verifica que los cambios cumplan con los estándares internos de calidad. Para los microservicios, esto suele ser una ejecución de linter para verificar el estilo del código y en busca de errores, una ejecución de prueba de unidad y el lanzamiento de una aplicación con elevación de contexto. Ahora, en la etapa de verificación, puede verificar las migraciones de la base de datos y la interacción de la capa DAO de la aplicación con la base de datos.



La creación de un contenedor con una base de datos y la distribución de conjuntos de parches aumenta el tiempo de inicio del contexto de Spring entre 1,5 y 10 segundos, según la potencia de la máquina en funcionamiento y la cantidad de conjuntos de parches.



Estas no son realmente pruebas unitarias, son pruebas para integrar la capa DAO de la aplicación con la base de datos.

Al llamar a una base de datos parte de un microservicio, decimos que está probando la integración de dos partes de un microservicio. Sin dependencias externas. Por lo tanto, estas pruebas son estables y se pueden ejecutar durante la fase de verificación. Arreglan el contrato de microservicio y base de datos, lo que brinda tranquilidad para futuras mejoras.



También es una forma práctica de depurar DAO. En lugar de llamar a RestController, simulando el comportamiento del usuario en algún escenario empresarial, llamamos inmediatamente al DAO con los argumentos necesarios.



Ejemplo de prueba 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))
}




Hay dos tareas relacionadas para ejecutar estas pruebas en la canalización de verificación:



  1. El agente de compilación puede estar potencialmente ocupado con el puerto 5432 de PostgreSQL estándar o con cualquiera estático. Nunca se sabe, alguien no apagó el contenedor con la base después de que se completaron las pruebas.
  2. De aquí la segunda tarea: es necesario apagar el contenedor después de que se completen las pruebas.


La biblioteca TestContainers resuelve estas dos tareas . Utiliza una imagen de la ventana acoplable existente para abrir el contenedor de la base de datos en el estado init.sql.



Ejemplo 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();
   }




Con el desarrollo y la depuración resueltos. Ahora necesitamos entregar los cambios en el esquema de la base de datos a producción.



¡Kubernetes es la respuesta! ¿Cual era tu pregunta?



Entonces, necesita automatizar algunos procesos de CI / CD. Tenemos un enfoque de ciudad en equipo probado y verdadero. Parecería, ¿dónde está el motivo de otro artículo?



Y hay una razón. Además del enfoque probado y verdadero, también hay problemas aburridos de una gran empresa.



  • No hay suficientes constructores de ciudades en equipo para todos.
  • Una licencia cuesta dinero.
  • La configuración de las máquinas virtuales compiladas se realiza a la antigua, a través de los repositorios con configuraciones y marionetas.
  • El acceso de los constructores a las redes objetivo debe hacerse a la antigua.
  • Los inicios de sesión y las contraseñas para avanzar los cambios en la base de datos también se almacenan a la antigua.


Y en todo esto, "a la antigua", el problema es que todos corren hacia un futuro brillante y el apoyo de Legacy ... ya sabes. Funciona y está bien. No funciona, lo solucionaremos más tarde. Algún día. Hoy no.



Digamos que ya está sumergido en el brillante futuro y ya tiene una infraestructura de Kubernetes. Incluso existe la oportunidad de generar otro microservicio, que se iniciará inmediatamente en esta infraestructura, recogerá la configuración y los secretos necesarios, tendrá el acceso necesario y se registrará en la infraestructura de la malla de servicios. Y toda esta felicidad puede ser obtenida por un desarrollador ordinario, sin involucrar a una persona con el rol * OPS. Recordamos que en Kubernetes existe un tipo de carga de trabajo, solo destinado a algún tipo de trabajo de servicio. Bueno, manejamos para hacer una aplicación en Kotlin + Spring-Liquibase, tratando de reutilizar lo máximo posible la infraestructura existente en la empresa para microservicios en JVM en kubera.



Reutilicemos los siguientes aspectos:



  • Generación del proyecto.
  • Desplegar.
  • Entrega de configuraciones y secretos.
  • Acceso.
  • Registro y entrega de registros a ELK.


Obtenemos tal canalización : Clickable









Ahora tenemos



  • Versiones del conjunto de cambios.
  • Los revisamos para ver si es factible actualización → reversión.
  • Pruebas de redacción para DAO. A veces incluso seguimos TDD: ejecutamos la depuración de DAO mediante pruebas. Las pruebas se realizan en una base de datos recién generada en TestContainers.
  • Ejecute la base de datos de Docker localmente en el puerto estándar. Estamos depurando, mirando lo que queda en la base de datos. Si es necesario, podemos administrar la base de datos local manualmente.
  • Entramos en los conjuntos de parches de prueba y lanzamiento automático con una canalización estándar en teamcity, por analogía con los microservicios. La canalización es un elemento secundario del microservicio que posee la base de datos.
  • No almacenamos créditos de la base de datos en la ciudad del equipo. Y no nos importan los accesos de los constructores virtuales.


Sé que para muchos esto no es una revelación. Pero como terminaste de leer, estaremos encantados de compartir tu experiencia en los comentarios.



All Articles