Comment nous avons réinventé en 2020 le processus de développement, de débogage et de fourniture de modifications de base de données pour produire

Il est 2020 dans la cour et vous avez déjà l'habitude d'entendre avec le bruit de fond: "Kubernetes est la réponse!", "Microservices!", "Service mesh!", "Sesuriti politiques!" Tout le monde court vers un avenir radieux.



Notre entreprise a une approche plus conservatrice en ce qui concerne les bases de données que les applications. La base de données ne tourne pas dans Kubernetes, mais sur du matériel ou dans une machine virtuelle. Nous avons un processus bien établi pour les modifications de la base de données de traitement des paiements, qui comprend de nombreux contrôles automatisés, un examen approfondi et une publication avec la participation du DBA. Le nombre de chèques et de personnes impliquées dans cette affaire affecte négativement le délai de mise sur le marché. D'autre part, il est débogué et vous permet d'apporter des modifications de manière fiable à la production, minimisant les chances de casser quelque chose. Et si quelque chose casse, les bonnes personnes sont déjà incluses dans le processus de réparation. Cette approche rend le travail du principal service de l'entreprise plus stable.



Nous démarrons la plupart des nouvelles bases de données relationnelles pour les microservices sur PostgreSQL. Un processus optimisé pour Oracle, bien que robuste, entraîne une complexité inutile pour les petites bases de données. Personne ne veut faire glisser des processus difficiles du passé vers un avenir radieux. Personne n'a commencé à travailler sur le processus d'un avenir radieux à l'avance. En conséquence, nous avons eu un manque de norme et de raznozhopitsu. Si vous voulez savoir quels problèmes cela a engendré et comment nous les avons résolus, bienvenue chez cat.











Problèmes que nous avons résolus



Il n'y a pas de normes uniformes pour la gestion des versions



Dans le meilleur des cas, il s'agit de fichiers SQL DDL qui se trouvent quelque part dans le répertoire db dans le référentiel avec le microservice. C'est très mauvais s'il ne s'agit que de l'état actuel de la base de données, différent sur le test et sur la production, et qu'il n'y a pas de scripts de référence pour le schéma de base de données.



Pendant le débogage, nous détruisons la base de test



"Je secoue un peu la base de données de test maintenant, ne vous inquiétez pas là-bas" - et je suis allé déboguer le code de changement de schéma nouvellement écrit sur la base de données de test. Parfois, cela prend beaucoup de temps et pendant tout ce temps, le circuit de test ne fonctionne pas.



Dans le même temps, le circuit de test peut se rompre dans la partie où d'autres microservices interagissent avec le microservice, dont le développeur a détruit la base.



Les méthodes DAO ne sont pas couvertes par les tests, ne sont pas validées en CI



Lors du développement et du débogage, les méthodes DAO sont appelées en tirant les poignées externes quelques couches ci-dessus. Cela expose des scénarios entiers de logique métier au lieu d'interactions spécifiques entre le microservice et la base de données.



Il n'y a aucune garantie que rien ne s'effondrera à l'avenir. La qualité et la maintenabilité du microservice en souffrent.



Non-isomorphisme des milieux



Si les boucles de modification sont livrées différemment pour le test et la production, vous ne pouvez pas être sûr que cela fonctionnera de la même manière. Surtout lorsque le développement et le débogage sont réellement effectués sur le test.



Les objets du test peuvent être créés sous le compte du développeur ou de l'application. Les subventions sont remises au hasard et accordent généralement tous les privilèges. Les subventions pour la demande sont accordées sur le principe «Je vois une erreur dans le journal - Je donne une subvention». Les subventions sont souvent oubliées à la sortie. Parfois, après la sortie, les tests smok ne couvrent pas toutes les nouvelles fonctionnalités et l'absence de subvention ne se déclenche pas immédiatement.



Processus lourd et fragile de mise en production



Le passage à la production a été effectué manuellement, mais par analogie avec le processus d'Oracle, via l'approbation du DBA, des responsables des versions et du déploiement par les ingénieurs de version.



Cela ralentit la sortie. Et en cas de problème, cela augmente les temps d'arrêt, compliquant l'accès du développeur à la base de données. Les scripts exec.sql et rollback.sql n'étaient souvent pas testés sur le test, car il n'y a pas de norme de patchs pour les non-Oracle, et le test allait jusqu'au bout.



Par conséquent, il arrive que les développeurs effectuent des modifications sur des services non critiques sans ce processus du tout.



Comment pouvez-vous faire pour être bon



Débogage sur une base de données locale dans un conteneur Docker



Pour certains, toutes les solutions techniques décrites dans l'article peuvent paraître évidentes. Mais pour une raison quelconque, d'année en année, je vois des gens qui marchent avec enthousiasme sur le même râteau.



Vous n'allez pas sur le serveur de test via ssh pour écrire et déboguer le code d'application, n'est-ce pas? Je trouve tout aussi absurde de développer et de déboguer le code de base de données sur une instance DB de test. Il y a des exceptions, parfois il est très difficile d'élever la base de données localement. Mais généralement, si nous parlons de quelque chose de léger et non hérité, alors il n'est pas difficile d'élever la base localement et de lancer toutes les migrations de manière cohérente. En contrepartie, vous obtiendrez une instance stable à vos côtés, qui ne sera pas embourbée par un autre développeur, à laquelle vous ne perdrez pas l'accès et sur laquelle vous avez les droits nécessaires au développement.



Voici un exemple de la facilité avec laquelle il est possible d'afficher une base de données locale:



Écrivons un fichier Dockerfile à deux lignes:



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


Dans init.sql, nous créons une base de données "propre", que nous espérons obtenir à la fois lors du test et en production. Il doit contenir:



  • Le propriétaire du schéma et le schéma lui-même.
  • Utilisateur d'application avec une autorisation d'utiliser le schéma.
  • EXTENSIONS requises


Exemple d'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";




Pour plus de commodité, vous pouvez ajouter la tâche db au Makefile, qui va (re) démarrer le conteneur avec la base et faire saillie du port pour la connexion:



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


Jeux de modifications de version avec quelque chose de standard de l'industrie



Cela semble également évident: vous devez écrire des migrations et les conserver dans le système de contrôle de version. Mais très souvent, je vois des scripts sql "nus", sans aucune liaison. Et cela signifie qu'il n'y a aucun contrôle sur le retour arrière et le retour arrière, par qui, quoi et quand a été pompé. Il n'y a même pas de garantie que vos scripts SQL puissent être exécutés sur la base de données de test et de production, car sa structure peut avoir changé.



En général, vous avez besoin de contrôle. Les systèmes de migration ne sont qu'une question de contrôle.

Nous n'entrerons pas dans une comparaison de différents systèmes de gestion de versions de schéma de base de données. FlyWay vs Liquibase n'est pas le sujet de cet article. Nous avons choisi Liquibase.



Nous version:



  • Structure DDL des objets de base de données (créer une table).
  • Contenu DML des tables de recherche (insertion, mise à jour).
  • Subventions DCL pour les applications UZ (sélection de subvention, insertion sur ...).


Lors du lancement et du débogage d'un microservice sur une base de données locale, un développeur sera confronté à la nécessité de s'occuper des subventions. Le seul moyen légal consiste à ajouter un script DCL à l'ensemble de modifications. Cela garantit que les subventions seront mises en vente.



Exemple de patchset 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;




Exemple 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 crée une table databasechangelog sur la base de données, où il note les changesets gonflés.

Calcule automatiquement le nombre d'ensembles de modifications dont vous avez besoin pour accéder à la base de données.



Il existe un maven et un plugin gradle avec la possibilité de générer un script à partir de plusieurs ensembles de modifications qui doivent être intégrés à la base de données.



Intégration du système de migration de base de données dans la phase de lancement de l'application



Il peut s'agir de n'importe quel adaptateur du système de contrôle de migration et du cadre sur lequel votre application est construite. Avec de nombreux frameworks, il est livré avec l'ORM. Par exemple Ruby-On-Rails, Yii2, Nest.JS.



Ce mécanisme est nécessaire pour lancer les migrations au démarrage du contexte d'application.

Par exemple:



  1. Dans la base de données de test, les ensembles de correctifs 001, 002, 003.
  2. Le pogromiste a développé les patchs 004, 005 et n'a pas déployé l'application pour le test.
  3. Déployez le test. Les patchs 004, 005 sont en cours de déploiement.


S'ils ne roulent pas, l'application ne démarre pas. La mise à jour continue ne tue pas les anciens pods.



Notre pile est JVM + Spring et nous n'utilisons pas ORM. Par conséquent, nous avions besoin de l' intégration Spring-Liquibase .



Nous avons une exigence de sécurité importante dans notre entreprise: l'utilisateur de l'application doit avoir un nombre limité de subventions et ne doit certainement pas avoir accès au niveau du propriétaire du schéma. Avec Spring-Liquibase, il est possible de lancer des migrations pour le compte de l'utilisateur propriétaire du schéma. Dans ce cas, le pool de connexions du niveau application application n'a pas accès à Liquibase DataSource. Par conséquent, l'application n'obtiendra pas l'accès de l'utilisateur propriétaire du schéma.



Exemple d'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"




Les tests DAO au stade CI vérifient



Notre entreprise a une telle étape CI - vérifier. À ce stade, les modifications sont vérifiées pour vérifier leur conformité aux normes de qualité internes. Pour les microservices, il s'agit généralement d'une exécution linter pour vérifier le style de code et pour les bogues, une exécution de test unitaire et un lancement d'application avec levage de contexte. Désormais, à l'étape de vérification, vous pouvez vérifier les migrations de base de données et l'interaction de la couche DAO d'application avec la base de données.



La création d'un conteneur avec une base de données et des jeux de correctifs en continu augmente l'heure de début du contexte Spring de 1,5 à 10 secondes, en fonction de la puissance de la machine en fonctionnement et du nombre de jeux de correctifs.



Ce ne sont pas vraiment des tests unitaires, ce sont des tests d'intégration de la couche DAO de l'application avec la base de données.

En appelant une base de données une partie d'un microservice, nous disons qu'elle teste l'intégration de deux parties d'un microservice. Pas de dépendances externes. Par conséquent, ces tests sont stables et peuvent être exécutés pendant la phase de vérification. Ils corrigent le contrat de microservice et de base de données, ce qui garantit les améliorations futures.



C'est également un moyen pratique de déboguer les DAO. Au lieu d'appeler RestController, simulant le comportement de l'utilisateur dans certains scénarios d'entreprise, nous appelons immédiatement le DAO avec les arguments requis.



Exemple de test 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))
}




Il existe deux tâches associées pour exécuter ces tests dans le pipeline de vérification:



  1. L'agent de construction peut potentiellement être occupé avec le port standard PostgreSQL 5432 ou n'importe quel port statique. On ne sait jamais, quelqu'un n'a pas sorti le conteneur avec la base une fois les tests terminés.
  2. À partir de là, la deuxième tâche: vous devez éteindre le conteneur une fois les tests terminés.


La bibliothèque TestContainers résout ces deux tâches . Il utilise une image docker existante pour faire apparaître le conteneur de base de données dans l'état init.sql.



Exemple d'utilisation 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();
   }




Avec le développement et le débogage compris. Nous devons maintenant fournir les modifications du schéma de base de données à la production.



Kubernetes est la réponse! Quelle était ta question?



Vous devez donc automatiser certains processus CI / CD. Nous avons une approche éprouvée de la ville d'équipe. Il semblerait, où est la raison d'un autre article?



Et il y a une raison. En plus de l'approche éprouvée, il existe également des problèmes ennuyeux d'une grande entreprise.



  • Il n'y a pas assez de team builders pour tout le monde.
  • Une licence coûte de l'argent.
  • Les paramètres des buildagents virtualok se font à l'ancienne, via des référentiels avec des configurations et des marionnettes.
  • L'accès des constructeurs aux réseaux cibles doit être scié à l'ancienne.
  • Les identifiants et les mots de passe pour les modifications ultérieures de la base de données sont également stockés à l'ancienne.


Et dans tout cela "à l'ancienne", le problème est que tout le monde court vers un avenir radieux et le soutien de Legacy ... vous savez. Ça marche et ça va. Cela ne fonctionne pas - nous y reviendrons plus tard. Un jour. Pas aujourd'hui.



Disons que vous avez déjà une jambe jusqu'aux genoux dans un avenir radieux et que vous avez déjà une infrastructure Kubernetes. Il est même possible de générer un autre microservice, qui démarrera immédiatement dans cette infrastructure, récupérera la configuration et les secrets nécessaires, aura l'accès nécessaire et s'enregistrera auprès de l'infrastructure de maillage de service. Et tout ce bonheur peut être obtenu par un développeur ordinaire, sans impliquer une personne avec le rôle * OPS. Nous rappelons que dans Kubernetes, il existe un type de charge de travail Job, uniquement destiné à une sorte de travail de service. Eh bien, nous avons conduit à faire une application sur Kotlin + Spring-Liquibase, en essayant de réutiliser autant que possible l'infrastructure existante dans l'entreprise pour les microservices sur JVM dans kubera.



Réutilisons les aspects suivants:



  • Génération du projet.
  • Déployer.
  • Livraison des configs et des secrets.
  • Accès.
  • Journalisation et livraison des logs à ELK.


Nous obtenons un tel pipeline : cliquable









Nous avons maintenant



  • Gestion des versions de l'ensemble de modifications.
  • Nous les vérifions pour une mise à jour de faisabilité → restauration.
  • Rédaction de tests pour DAO. Parfois, nous suivons même TDD: nous exécutons le débogage DAO à l'aide de tests. Les tests sont effectués sur une base de données récemment créée dans TestContainers.
  • Exécutez la base de données docker localement sur un port standard. Nous sommes en train de déboguer, en regardant ce qui reste dans la base de données. Si nécessaire, nous pouvons gérer la base de données locale manuellement.
  • Nous intégrons les patchs de test et de publication automatique avec un pipeline standard dans teamcity, par analogie avec les microservices. Le pipeline est un enfant du microservice propriétaire de la base de données.
  • Nous ne stockons pas les crédits de la base de données dans Team City. Et nous ne nous soucions pas des accès des constructeurs virtuels.


Je sais que pour beaucoup ce n'est pas une révélation. Mais puisque vous avez fini de lire, nous serons heureux de partager votre expérience dans les commentaires.



All Articles