Bagikan melalui


Menggunakan server database yang berjalan sebagai kontainer

Tip

Konten ini adalah kutipan dari eBook, .NET Microservices Architecture for Containerized .NET Applications, tersedia di .NET Docs atau sebagai PDF yang dapat diunduh gratis dan dapat dibaca secara offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Anda dapat memiliki database Anda (SQL Server, PostgreSQL, MySQL, dll.) di server mandiri reguler, di kluster lokal, atau di layanan PaaS di cloud seperti Azure SQL DB. Namun, untuk lingkungan pengembangan dan pengujian, menjalankan database Anda sebagai kontainer nyaman, karena Anda tidak memiliki dependensi eksternal dan hanya menjalankan docker-compose up perintah memulai seluruh aplikasi. Memiliki database tersebut sebagai kontainer juga bagus untuk pengujian integrasi, karena database dimulai dalam kontainer dan selalu diisi dengan data sampel yang sama, sehingga pengujian dapat lebih dapat diprediksi.

Dalam eShopOnContainers, ada kontainer bernama sqldata, seperti yang ditentukan dalam file docker-compose.yml, yang menjalankan SQL Server untuk instans Linux dengan database SQL untuk semua layanan mikro yang membutuhkannya.

Titik kunci dalam layanan mikro adalah bahwa setiap layanan mikro memiliki data terkait, sehingga harus memiliki database sendiri. Namun, database dapat berada di mana saja. Dalam hal ini, semuanya berada dalam kontainer yang sama untuk menjaga persyaratan memori Docker serendah mungkin. Perlu diingat bahwa ini adalah solusi yang cukup baik untuk pengembangan dan, mungkin, pengujian tetapi tidak untuk produksi.

Kontainer SQL Server dalam aplikasi sampel dikonfigurasi dengan kode YAML berikut dalam file docker-compose.yml, yang dijalankan saat Anda menjalankan docker-compose up. Perhatikan bahwa kode YAML telah mengkonsolidasikan informasi konfigurasi dari file docker-compose.yml generik dan file docker-compose.override.yml. (Biasanya Anda akan memisahkan pengaturan lingkungan dari informasi dasar atau statis yang terkait dengan gambar SQL Server.)

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

Dengan cara yang sama, alih-alih menggunakan docker-compose, perintah berikut dapat docker run menjalankan kontainer tersebut:

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

Namun, jika Anda menyebarkan aplikasi multikontainer seperti eShopOnContainers, lebih mudah untuk menggunakan docker-compose up perintah sehingga menyebarkan semua kontainer yang diperlukan untuk aplikasi.

Saat Anda memulai kontainer SQL Server ini untuk pertama kalinya, kontainer menginisialisasi SQL Server dengan kata sandi yang Anda berikan. Setelah SQL Server berjalan sebagai kontainer, Anda dapat memperbarui database dengan menyambungkan melalui koneksi SQL reguler, seperti dari kode SQL Server Management Studio, Visual Studio, atau C#.

Aplikasi eShopOnContainers menginisialisasi setiap database layanan mikro dengan data sampel dengan menyemainya dengan data saat startup, seperti yang dijelaskan di bagian berikut.

Memiliki SQL Server berjalan sebagai kontainer tidak hanya berguna untuk demo ketika Anda mungkin tidak memiliki akses ke instans SQL Server. Seperti disebutkan, ini juga bagus untuk lingkungan pengembangan dan pengujian sehingga Anda dapat dengan mudah menjalankan pengujian integrasi mulai dari gambar SQL Server yang bersih dan data yang diketahui dengan menyemai data sampel baru.

Sumber daya tambahan

Seeding dengan data pengujian pada startup aplikasi Web

Untuk menambahkan data ke database saat aplikasi dimulai, Anda dapat menambahkan kode seperti berikut ini ke Main metode di Program kelas proyek 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();
    }
}

Ada peringatan penting saat menerapkan migrasi dan menyemai database selama startup kontainer. Karena server database mungkin tidak tersedia karena alasan apa pun, Anda harus menangani percobaan ulang sambil menunggu server tersedia. Logika coba lagi ini ditangani oleh MigrateDbContext() metode ekstensi, seperti yang ditunjukkan dalam kode berikut:

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

Kode berikut di kelas CatalogContextSeed kustom mengisi data.

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

Saat Anda menjalankan pengujian integrasi, memiliki cara untuk menghasilkan data yang konsisten dengan pengujian integrasi Anda berguna. Mampu membuat semuanya dari awal, termasuk instans SQL Server yang berjalan pada kontainer, sangat bagus untuk lingkungan pengujian.

Database EF Core InMemory versus SQL Server berjalan sebagai kontainer

Pilihan lain yang baik saat menjalankan pengujian adalah menggunakan penyedia database Entity Framework InMemory. Anda dapat menentukan konfigurasi tersebut dalam metode ConfigureServices dari kelas Startup di proyek API Web Anda:

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

Ada pengambilan penting, meskipun. Database dalam memori tidak mendukung banyak batasan yang khusus untuk database tertentu. Misalnya, Anda dapat menambahkan indeks unik pada kolom dalam model EF Core Anda dan menulis pengujian terhadap database dalam memori Anda untuk memeriksa bahwa itu tidak memungkinkan Anda menambahkan nilai duplikat. Tetapi saat Anda menggunakan database dalam memori, Anda tidak dapat menangani indeks unik pada kolom. Oleh karena itu, database dalam memori tidak bersifat sama persis dengan database SQL Server nyata—database tersebut tidak meniru batasan khusus database.

Meskipun demikian, database dalam memori masih berguna untuk pengujian dan pembuatan prototipe. Tetapi jika Anda ingin membuat pengujian integrasi akurat yang mempertimbangkan perilaku implementasi database tertentu, Anda perlu menggunakan database nyata seperti SQL Server. Untuk tujuan itu, menjalankan SQL Server dalam kontainer adalah pilihan yang bagus dan lebih akurat daripada penyedia database EF Core InMemory.

Menggunakan layanan cache Redis yang berjalan dalam kontainer

Anda dapat menjalankan Redis pada kontainer, terutama untuk pengembangan dan pengujian dan untuk skenario bukti konsep. Skenario ini nyaman, karena Anda dapat menjalankan semua dependensi Anda pada kontainer—bukan hanya untuk mesin pengembangan lokal Anda, tetapi untuk lingkungan pengujian Anda di alur CI/CD Anda.

Namun, ketika Anda menjalankan Redis dalam produksi, lebih baik mencari solusi ketersediaan tinggi seperti Redis Microsoft Azure, yang berjalan sebagai PaaS (Platform as a Service). Dalam kode, Anda hanya perlu mengubah string koneksi Anda.

Redis menyediakan gambar Docker dengan Redis. Gambar tersebut tersedia dari Docker Hub di URL ini:

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

Anda dapat langsung menjalankan kontainer Docker Redis dengan menjalankan perintah Docker CLI berikut di prompt perintah Anda:

docker run --name some-redis -d redis

Gambar Redis mencakup expose:6379 (port yang digunakan oleh Redis), sehingga penautan kontainer standar akan membuatnya tersedia secara otomatis ke kontainer yang ditautkan.

Di eShopOnContainers, basket-api layanan mikro menggunakan cache Redis yang berjalan sebagai kontainer. Kontainer basketdata tersebut ditentukan sebagai bagian dari file docker-compose.yml multikontainer, seperti yang ditunjukkan dalam contoh berikut:

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

Kode dalam docker-compose.yml ini menentukan kontainer bernama basketdata berdasarkan gambar redis dan menerbitkan port 6379 secara internal. Konfigurasi ini berarti bahwa konfigurasi ini hanya akan dapat diakses dari kontainer lain yang berjalan dalam host Docker.

Terakhir, dalam file docker-compose.override.yml, basket-api layanan mikro untuk sampel eShopOnContainers menentukan string koneksi yang akan digunakan untuk kontainer Redis tersebut:

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

Seperti disebutkan sebelumnya, nama layanan basketdata mikro diselesaikan oleh DNS jaringan internal Docker.