Partager via


Utiliser un serveur de base de données s’exécutant en tant que conteneur

Conseil / Astuce

Ce contenu est un extrait du livre électronique 'Architecture des microservices .NET pour les applications .NET conteneurisées', disponible sur .NET Docs ou en tant que PDF téléchargeable gratuitement, lisible hors ligne.

Architecture de microservices .NET pour les applications .NET conteneurisées - vignette de couverture du livre électronique.

Vous pouvez avoir vos bases de données (SQL Server, PostgreSQL, MySQL, etc.) sur des serveurs autonomes standard, dans des clusters locaux ou dans des services PaaS dans le cloud comme Azure SQL DB. Toutefois, pour les environnements de développement et de test, l’exécution de vos bases de données en tant que conteneurs est pratique, car vous n’avez pas de dépendance externe et l’exécution de la commande démarre l’ensemble de l’application docker-compose up . L’utilisation de ces bases de données en tant que conteneurs est également idéale pour les tests d’intégration, car la base de données est démarrée dans le conteneur et est toujours remplie avec les mêmes exemples de données, de sorte que les tests peuvent être plus prévisibles.

Dans eShopOnContainers, il existe un conteneur nommé sqldata, tel que défini dans le fichier docker-compose.yml , qui exécute une instance SQL Server pour Linux avec les bases de données SQL pour tous les microservices qui en ont besoin.

Un point clé dans les microservices est que chaque microservice possède ses données associées, de sorte qu’il doit avoir sa propre base de données. Toutefois, les bases de données peuvent être n’importe où. Dans ce cas, ils se trouvent tous dans le même conteneur pour conserver les exigences de mémoire Docker aussi faibles que possible. Gardez à l’esprit qu’il s’agit d’une solution suffisante pour le développement et, peut-être, de tester, mais pas pour la production.

Le conteneur SQL Server de l’exemple d’application est configuré avec le code YAML suivant dans le fichier docker-compose.yml, qui est exécuté lors de l’exécution docker-compose up. Notez que le code YAML contient des informations de configuration consolidées à partir du fichier de docker-compose.yml générique et du fichier docker-compose.override.yml. (Généralement, vous séparez les paramètres d’environnement de l’information de base ou statique liée à l’image SQL Server.)

  sqldata:
    image: mcr.microsoft.com/mssql/server:2017-latest
    environment:
      - SA_PASSWORD=Pass@word
      - ACCEPT_EULA=Y
    ports:
      - "5434:1433"

De la même façon, au lieu d’utiliser docker-compose, la commande suivante docker run peut exécuter ce conteneur :

docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Pass@word' -p 5433:1433 -d mcr.microsoft.com/mssql/server:2017-latest

Toutefois, si vous déployez une application multicontainers comme eShopOnContainers, il est plus pratique d’utiliser la docker-compose up commande afin qu’elle déploie tous les conteneurs requis pour l’application.

Lorsque vous démarrez ce conteneur SQL Server pour la première fois, le conteneur initialise SQL Server avec le mot de passe que vous fournissez. Une fois que SQL Server est en cours d’exécution en tant que conteneur, vous pouvez mettre à jour la base de données en vous connectant via n’importe quelle connexion SQL normale, comme à partir de SQL Server Management Studio, de Visual Studio ou de code C#.

L’application eShopOnContainers initialise chaque base de données de microservice avec des exemples de données en y insérant des données au démarrage, comme expliqué dans la section suivante.

L’exécution de SQL Server en tant que conteneur n’est pas seulement utile pour une démonstration où vous n’avez peut-être pas accès à une instance de SQL Server. Comme indiqué, il est également idéal pour les environnements de développement et de test afin que vous puissiez facilement exécuter des tests d’intégration à partir d’une image SQL Server propre et de données connues en amorçage de nouveaux exemples de données.

Ressources supplémentaires

Alimentation à l’aide de données de test au démarrage de l’application web

Pour ajouter des données à la base de données au démarrage de l’application, vous pouvez ajouter du code comme suit à la méthode dans la MainProgram classe du projet d’API web :

public static int Main(string[] args)
{
    var configuration = GetConfiguration();

    Log.Logger = CreateSerilogLogger(configuration);

    try
    {
        Log.Information("Configuring web host ({ApplicationContext})...", AppName);
        var host = CreateHostBuilder(configuration, args);

        Log.Information("Applying migrations ({ApplicationContext})...", AppName);
        host.MigrateDbContext<CatalogContext>((context, services) =>
        {
            var env = services.GetService<IWebHostEnvironment>();
            var settings = services.GetService<IOptions<CatalogSettings>>();
            var logger = services.GetService<ILogger<CatalogContextSeed>>();

            new CatalogContextSeed()
                .SeedAsync(context, env, settings, logger)
                .Wait();
        })
        .MigrateDbContext<IntegrationEventLogContext>((_, __) => { });

        Log.Information("Starting web host ({ApplicationContext})...", AppName);
        host.Run();

        return 0;
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", AppName);
        return 1;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

Il existe une mise en garde importante lors de l’application des migrations et de l’amorçage d’une base de données au démarrage du conteneur. Étant donné que le serveur de base de données peut ne pas être disponible pour une raison quelconque, vous devez gérer les nouvelles tentatives en attendant que le serveur soit disponible. Cette logique de nouvelle tentative est gérée par la MigrateDbContext() méthode d’extension, comme indiqué dans le code suivant :

public static IWebHost MigrateDbContext<TContext>(
    this IWebHost host,
    Action<TContext,
    IServiceProvider> seeder)
      where TContext : DbContext
{
    var underK8s = host.IsInKubernetes();

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;

        var logger = services.GetRequiredService<ILogger<TContext>>();

        var context = services.GetService<TContext>();

        try
        {
            logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);

            if (underK8s)
            {
                InvokeSeeder(seeder, context, services);
            }
            else
            {
                var retry = Policy.Handle<SqlException>()
                    .WaitAndRetry(new TimeSpan[]
                    {
                    TimeSpan.FromSeconds(3),
                    TimeSpan.FromSeconds(5),
                    TimeSpan.FromSeconds(8),
                    });

                //if the sql server container is not created on run docker compose this
                //migration can't fail for network related exception. The retry options for DbContext only
                //apply to transient exceptions
                // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
                retry.Execute(() => InvokeSeeder(seeder, context, services));
            }

            logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
            if (underK8s)
            {
                throw;          // Rethrow under k8s because we rely on k8s to re-run the pod
            }
        }
    }

    return host;
}

Le code suivant dans la classe CatalogContextSeed personnalisée remplit les données.

public class CatalogContextSeed
{
    public static async Task SeedAsync(IApplicationBuilder applicationBuilder)
    {
        var context = (CatalogContext)applicationBuilder
            .ApplicationServices.GetService(typeof(CatalogContext));
        using (context)
        {
            context.Database.Migrate();
            if (!context.CatalogBrands.Any())
            {
                context.CatalogBrands.AddRange(
                    GetPreconfiguredCatalogBrands());
                await context.SaveChangesAsync();
            }
            if (!context.CatalogTypes.Any())
            {
                context.CatalogTypes.AddRange(
                    GetPreconfiguredCatalogTypes());
                await context.SaveChangesAsync();
            }
        }
    }

    static IEnumerable<CatalogBrand> GetPreconfiguredCatalogBrands()
    {
        return new List<CatalogBrand>()
       {
           new CatalogBrand() { Brand = "Azure"},
           new CatalogBrand() { Brand = ".NET" },
           new CatalogBrand() { Brand = "Visual Studio" },
           new CatalogBrand() { Brand = "SQL Server" }
       };
    }

    static IEnumerable<CatalogType> GetPreconfiguredCatalogTypes()
    {
        return new List<CatalogType>()
        {
            new CatalogType() { Type = "Mug"},
            new CatalogType() { Type = "T-Shirt" },
            new CatalogType() { Type = "Backpack" },
            new CatalogType() { Type = "USB Memory Stick" }
        };
    }
}

Lorsque vous exécutez des tests d’intégration, vous pouvez générer des données cohérentes avec vos tests d’intégration. La possibilité de créer tout à partir de zéro, y compris une instance de SQL Server s’exécutant sur un conteneur, est idéale pour les environnements de test.

Base de données Ef Core InMemory et SQL Server s’exécutant en tant que conteneur

Un autre bon choix lors de l’exécution de tests consiste à utiliser le fournisseur de base de données Entity Framework InMemory. Vous pouvez spécifier cette configuration dans la méthode ConfigureServices de la classe Startup dans votre projet d’API web :

public class Startup
{
    // Other Startup code ...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IConfiguration>(Configuration);
        // DbContext using an InMemory database provider
        services.AddDbContext<CatalogContext>(opt => opt.UseInMemoryDatabase());
        //(Alternative: DbContext using a SQL Server provider
        //services.AddDbContext<CatalogContext>(c =>
        //{
            // c.UseSqlServer(Configuration["ConnectionString"]);
            //
        //});
    }

    // Other Startup code ...
}

Il y a cependant un hic important. La base de données en mémoire ne prend pas en charge de nombreuses contraintes spécifiques à une base de données particulière. Par exemple, vous pouvez ajouter un index unique sur une colonne dans votre modèle EF Core et écrire un test sur votre base de données en mémoire pour vérifier qu’elle ne vous permet pas d’ajouter une valeur en double. Toutefois, lorsque vous utilisez la base de données en mémoire, vous ne pouvez pas gérer des index uniques sur une colonne. Par conséquent, la base de données en mémoire ne se comporte pas exactement comme une base de données SQL Server réelle. Elle n’émule pas les contraintes spécifiques à la base de données.

Même si, une base de données en mémoire est toujours utile pour le test et le prototypage. Toutefois, si vous souhaitez créer des tests d’intégration précis qui tiennent compte du comportement d’une implémentation de base de données spécifique, vous devez utiliser une base de données réelle comme SQL Server. À cet effet, l’exécution de SQL Server dans un conteneur est un excellent choix et plus précis que le fournisseur de base de données EF Core InMemory.

Utilisation d’un service de cache Redis s’exécutant dans un conteneur

Vous pouvez exécuter Redis sur un conteneur, en particulier pour le développement et le test et pour les scénarios de preuve de concept. Ce scénario est pratique, car vous pouvez avoir toutes vos dépendances s’exécutant sur des conteneurs, non seulement pour vos machines de développement locales, mais pour vos environnements de test dans vos pipelines CI/CD.

Toutefois, lorsque vous exécutez Redis en production, il est préférable de rechercher une solution de haute disponibilité comme Redis Microsoft Azure, qui s’exécute en tant que PaaS (Platform as a Service). Dans votre code, vous devez simplement modifier vos chaînes de connexion.

Redis fournit une image Docker avec Redis. Cette image est disponible à partir de Docker Hub à cette URL :

https://hub.docker.com/_/redis/

Vous pouvez exécuter directement un conteneur Docker Redis en exécutant la commande Docker CLI suivante dans votre invite de commandes :

docker run --name some-redis -d redis

L’image Redis inclut expose :6379 (port utilisé par Redis), de sorte que la liaison de conteneur standard la rend automatiquement disponible pour les conteneurs liés.

Dans eShopOnContainers, le basket-api microservice utilise un cache Redis s’exécutant en tant que conteneur. Ce basketdata conteneur est défini dans le cadre du fichier docker-compose.yml multiconteneur , comme illustré dans l’exemple suivant :

#docker-compose.yml file
#...
  basketdata:
    image: redis
    expose:
      - "6379"

Ce code dans l’docker-compose.yml définit un conteneur nommé basketdata en fonction de l’image redis et publie le port 6379 en interne. Cette configuration signifie qu’elle sera accessible uniquement à partir d’autres conteneurs s’exécutant dans l’hôte Docker.

Enfin, dans le fichier docker-compose.override.yml , le basket-api microservice de l’exemple eShopOnContainers définit la chaîne de connexion à utiliser pour ce conteneur Redis :

  basket-api:
    environment:
      # Other data ...
      - ConnectionString=basketdata
      - EventBusConnection=rabbitmq

Comme mentionné précédemment, le nom du microservice basketdata est résolu par le DNS de réseau interne de Docker.