Bagikan melalui


Menggunakan server database yang berjalan sebagai kontainer

Petunjuk / Saran

Konten ini adalah kutipan dari eBook, Arsitektur Layanan Mikro .NET untuk Aplikasi .NET Kontainer, tersedia di .NET Docs atau sebagai PDF gratis yang dapat diunduh yang dapat dibaca secara offline.

Arsitektur Layanan Mikro .NET untuk Aplikasi .NET Dalam Kontainer: gambar kecil sampul eBook.

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, membuat database Anda berjalan 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 didefinisikan dalam file docker-compose.yml , yang menjalankan instans SQL Server untuk 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, mereka semua 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 docker run dapat 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 multi-kontainer seperti eShopOnContainers, lebih mudah untuk menggunakan docker-compose up perintah sehingga menyebarkan semua kontainer yang diperlukan untuk aplikasi.

Ketika 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 apa pun, seperti dari SQL Server Management Studio, Visual Studio, atau kode 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 yang berjalan sebagai kontainer tidak hanya berguna untuk demo di mana Anda mungkin tidak memiliki akses ke instans SQL Server. Seperti yang 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

Memasukkan data uji saat startup aplikasi web

Untuk menambahkan data ke database saat aplikasi dimulai, Anda dapat menambahkan kode seperti berikut 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 yang 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 syarat penting, namun. 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 berulah 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 memperhitungkan 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 memiliki semua dependensi yang berjalan 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 image Docker untuk 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 menyertakan expose:6379 (port yang digunakan oleh Redis), sehingga penautan kontainer standar akan membuatnya tersedia secara otomatis bagi kontainer yang saling terhubung.

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

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

Kode dalam docker-compose.yml ini mendefinisikan 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 mikro basketdata diselesaikan oleh DNS jaringan internal Docker.