次の方法で共有


コンテナーとして実行されているデータベース サーバーを使用する

ヒント

このコンテンツは、.NET Docs で入手できる、またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できる、コンテナー化された .NET アプリケーションの電子ブックである .NET マイクロサービス アーキテクチャからの抜粋です。

コンテナー化された .NET アプリケーションの .NET マイクロサービス アーキテクチャの電子ブックの表紙サムネイル。

データベース (SQL Server、PostgreSQL、MySQL など) は、通常のスタンドアロン サーバー、オンプレミス クラスター、または Azure SQL DB などのクラウド内の PaaS サービスに置くことができます。 ただし、開発環境とテスト環境では、データベースをコンテナーとして実行すると便利です。外部依存関係がなく、 docker-compose up コマンドを実行するだけで、アプリケーション全体が起動します。 これらのデータベースをコンテナーとして使用することは、統合テストにも適しています。これは、データベースがコンテナーで開始され、常に同じサンプル データが設定されるため、テストの予測が可能になるためです。

eShopOnContainers には、docker-compose.yml ファイルで定義されている sqldata という名前のコンテナーがあり、 必要 なすべてのマイクロサービスに対して SQL データベースを使用して SQL Server for Linux インスタンスを実行します。

マイクロサービスの重要なポイントは、各マイクロサービスが関連するデータを所有しているため、独自のデータベースを持つ必要があるということです。 ただし、データベースは任意の場所に置くことができます。 この場合、Docker のメモリ要件を可能な限り低く保つために、これらはすべて同じコンテナー内にあります。 これは開発やテストには十分なソリューションですが、運用環境には適していません。

サンプル アプリケーションの SQL Server コンテナーは、docker-compose.yml ファイル内の次の YAML コードで構成されます。これは、 docker-compose upの実行時に実行されます。 YAML コードには、汎用docker-compose.yml ファイルとdocker-compose.override.yml ファイルからの構成情報が統合されていることに注意してください。 (通常、環境設定は、SQL Server イメージに関連する基本または静的な情報から分離します)。

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

同様に、 docker-composeを使用する代わりに、次の docker run コマンドでそのコンテナーを実行できます。

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

ただし、eShopOnContainers などのマルチコンテナー アプリケーションをデプロイする場合は、 docker-compose up コマンドを使用して、アプリケーションに必要なすべてのコンテナーをデプロイする方が便利です。

この SQL Server コンテナーを初めて起動すると、指定したパスワードを使用して SQL Server が初期化されます。 SQL Server がコンテナーとして実行されたら、SQL Server Management Studio、Visual Studio、C# コードなどの通常の SQL 接続を介して接続することで、データベースを更新できます。

eShopOnContainers アプリケーションは、次のセクションで説明するように、起動時にデータをシード処理することで、各マイクロサービス データベースをサンプル データで初期化します。

SQL Server をコンテナーとして実行することは、SQL Server のインスタンスにアクセスできない可能性があるデモに役立つだけではありません。 既に説明したように、新しいサンプル データをシードすることで、クリーンな SQL Server イメージと既知のデータから開始して統合テストを簡単に実行できるように、開発環境とテスト環境にも適しています。

その他のリソース

Web アプリケーションの起動時にテスト データを使用したシード処理

アプリケーションの起動時にデータベースにデータを追加するには、Web API プロジェクトの Main クラスの Program メソッドに次のようなコードを追加します。

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

コンテナーの起動時に移行を適用し、データベースをシード処理する際には、重要な注意事項があります。 データベース サーバーは何らかの理由で使用できない可能性があるため、サーバーが使用可能になるのを待っている間に再試行を処理する必要があります。 この再試行ロジックは、次のコードに示すように、 MigrateDbContext() 拡張メソッドによって処理されます。

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;
}

カスタム CatalogContextSeed クラスの次のコードは、データを設定します。

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

統合テストを実行する場合は、統合テストと一貫性のあるデータを生成する方法が役立ちます。 コンテナーで実行されている SQL Server のインスタンスを含め、すべてをゼロから作成できることは、テスト環境に適しています。

EF Core InMemory データベースと、コンテナーとして実行されている SQL Server の比較

テストを実行する際のもう 1 つの良い選択肢は、Entity Framework InMemory データベース プロバイダーを使用することです。 その構成は、Web API プロジェクトの Startup クラスの ConfigureServices メソッドで指定できます。

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 ...
}

しかし、重要なキャッチがあります。 メモリ内データベースでは、特定のデータベースに固有の制約が多数サポートされていません。 たとえば、EF Core モデルの列に一意のインデックスを追加し、メモリ内データベースに対してテストを記述して、重複する値を追加できないかどうかを確認できます。 ただし、メモリ内データベースを使用している場合、列の一意のインデックスを処理することはできません。 そのため、メモリ内データベースは実際の SQL Server データベースとまったく同じように動作しません。データベース固有の制約はエミュレートされません。

それでも、インメモリ データベースはテストとプロトタイプ作成に引き続き役立ちます。 ただし、特定のデータベース実装の動作を考慮した正確な統合テストを作成する場合は、SQL Server などの実際のデータベースを使用する必要があります。 そのため、コンテナーで SQL Server を実行することは、EF Core InMemory データベース プロバイダーよりも優れた選択肢であり、より正確です。

コンテナーで実行されている Redis Cache サービスの使用

Redis は、特に開発とテスト、および概念実証シナリオの場合に、コンテナーで実行できます。 このシナリオは便利です。ローカル開発マシンだけでなく、CI/CD パイプラインのテスト環境でも、すべての依存関係をコンテナーで実行できるためです。

ただし、運用環境で Redis を実行する場合は、PaaS (サービスとしてのプラットフォーム) として実行される Redis Microsoft Azure などの高可用性ソリューションを検索することをお勧めします。 コードでは、接続文字列を変更するだけで済みます。

Redis は、Redis を使用して Docker イメージを提供します。 そのイメージは、次の URL で Docker Hub から入手できます。

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

コマンド プロンプトで次の Docker CLI コマンドを実行することで、Docker Redis コンテナーを直接実行できます。

docker run --name some-redis -d redis

Redis イメージには、expose:6379 (Redis で使用されるポート) が含まれているため、標準のコンテナー リンクを使用すると、リンクされたコンテナーで自動的に使用できるようになります。

eShopOnContainers では、 basket-api マイクロサービスは、コンテナーとして実行されている Redis キャッシュを使用します。 その basketdata コンテナーは、次の例に示すように、マルチコンテナー docker-compose.yml ファイルの一部として定義されます。

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

docker-compose.ymlのこのコードでは、redis イメージに基づいて basketdata という名前のコンテナーを定義し、ポート 6379 を内部で発行します。 この構成は、Docker ホスト内で実行されている他のコンテナーからのみアクセス可能であることを意味します。

最後に、 docker-compose.override.yml ファイルでは、eShopOnContainers サンプルの basket-api マイクロサービスによって、その Redis コンテナーに使用する接続文字列が定義されます。

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

前述のように、マイクロサービス basketdata の名前は、Docker の内部ネットワーク DNS によって解決されます。