Kubernetes Job Kullanarak SQL Migration İşlemlerini Gerçekleştirmek

Teknolojinin sürekli geliştiği ve değiştiği gibi, içerisinde çalıştığımız uygulamanın database schema’sı da her yeni implemente ettiğimiz özellik ile değişebilmekte. Dolayısıyla domain model’lerinde gerçekleştirilecek olan değişikliklerin, database schema’sı üzerinde de uygulanabilmesi için bir migration stratejisi izlememiz gerekmektedir.

Bu makale kapsamında ise uygulamalarımızı kubernetes ortamına deploy ederken, migration işlemlerini kubernetes jobs kullanarak nasıl gerçekleştirebileceğimizi göstermeye çalışacağım.

Gereksinimler

  • Docker
  • Kubernetes
  • Helm3
  • MSSQL
  • dotnet-ef tool

Ben geliştirme ortamı olarak Docker Desktop’un Kubernetes özelliğini kullanacağım.

Örnek Projeye Göz Atalım

Öncelikle migration işlemleri için .NET 5 ve EF Core kullanarak hazırladığım buradaki  örnek projeyi inceleyelim.

Migration işlemlerini ana uygulamadan ayrı bir şekilde gerçekleştirebilmek için, “Todo.DbMigration” adında bir console application oluşturdum. İçerisinde ise basit olarak “IDesignTimeDbContextFactory” interface’ini, “Todo.Data” library’sindeki “TodoDbContext” i kullanarak implemente ettim.

using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using Todo.Data;

namespace Todo.DbMigration
{
    public class TodoDbContextFactory : IDesignTimeDbContextFactory<TodoDbContext>
    {
        public TodoDbContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables()
                .Build();

            var dbContextOptionsBuilder = new DbContextOptionsBuilder<TodoDbContext>();
 
            var connectionString = configuration
                        .GetConnectionString("SqlConnectionString");
        
            dbContextOptionsBuilder.UseSqlServer(connectionString, x => x.MigrationsAssembly("Todo.DbMigration"));
        
            return new TodoDbContext(dbContextOptionsBuilder.Options);
        }
    }
}

Todo.Data” library’sindeki db context ise aşağıdaki gibidir.

using Microsoft.EntityFrameworkCore;
using Todo.Data.Models;

namespace Todo.Data
{
    public class TodoDbContext : DbContext
    {
        public TodoDbContext(DbContextOptions<TodoDbContext> options)
            : base(options)
        {

        }

        public DbSet<TodoEntity> Todos { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TodoEntity>()
                .HasKey(x => x.Id);
            modelBuilder.Entity<TodoEntity>()
                .Property(p => p.Name)
                .HasMaxLength(150)
                .IsRequired();
        }
    }
}

Initial migration’ı oluşturabilmek için ise, projenin root klasöründe aşağıdaki komutu çalıştırdım.

dotnet ef migrations add InitialCreate --project ./Todo.Migration

Kubernetes Jobs

Kubernetes jobs, bize sonlu işlemlerimizi çalıştırabileceğimiz bir yapı sunmaktadır. Tıpkı bir controller’ın fail olmuş bir pod’u “reschedule” veya “restrart” ettiği gibi, kubernetes jobs da sonlu olan işlemlerimizin başarıyla çalışmasını sağlamaktadır.

Kubernetes jobs ile oluşturulan bir pod eğer fail olmadı ve başarıyla exit oldu ise, ilgili job başarıyla tamamlanmış olarak kabul edilmektedir. Bu işlemi ise deploy ettiğimiz bir job’ın “completion” durumunu sorgulayarak gerçekleştirebiliriz.

Ayrıca bir job silindiğinde ise oluşturmuş olduğu pod’lar da otomatik olarak silinmektedir. Fakat job tamamlandığında otomatik olarak silinmemektedir. Silinme işlemi ise job’ın completion durumu sorgulanarak, manuel olarak gerçekleştirilmelidir. Bu işleme ise birazdan değineceğiz.

Toparlamak gerekirse kubernetes jobs, özellikle batch veya migration gibi senaryolar için harika bir uyum sağlamaktadır.

Job Helm Chart Template’i Oluşturalım

Ben deployment işlemleri için productivity’i arttırdığı, tekrar kullanılabilirliği sağladığı ve bir standardizasyon kazandırdığı için helm’i kullanmayı tercih ediyorum. Bu yüzden job deployment işlemi için de bir helm chart kullanacağız.

Bunun için öncelikle bir job helm chart template’i oluşturmamız gerekmektedir.

Şimdi projenin root klasöründe bulunan “helm-charts” path’ine giderek, aşağıdaki komut ile bir initial helm chart template’i oluşturalım.

helm create migration-job

Ardından chart içerisindeki “templates” klasörü içerisinde bulunan “_helpers.tpl” dosyası hariç geriye kalan tüm dosyaları silelim.

Şimdi “templates” klasörü altında “job.yaml” dosyasını aşağıdaki gibi tanımlayalım.

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "migration-job.fullname" . }}
spec:
  backoffLimit: 0
  template:
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
      restartPolicy: Never
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}

Bu spec’deki dikkate almamız gereken kısım restrartPolicy ve backoffLimit dir. Ben herhangi bir başarı veya hata durumlarında migration gibi işlemler için pod’un/container’ın tekrar başlatılmasını istemediğimden dolayı bu policy’leri “0” ve “Never” olarak ayarlıyorum. (Elbette bazı istisnai durumlar karşısında bunun da garantisi yok) Aksi taktirde “backoffLimit” default olarak “6” olduğu için, job, hata veren pod’u tekrar ve tekrar başlatmayı deneyecektir.

Ayrıca bu configuration ile, o anki oluşan hatanın neyden dolayı kaynaklandığını bulabilmemiz de kolaylaşacaktır.

Value dosyasını ise aşağıdaki gibi güncelleyelim.

image:
  repository: mytodoapp-migration
  tag: "v1"

nodeSelector:
  beta.kubernetes.io/os: linux

Böylece helm chart template’i hazır durumda. Şimdi tek yapmamız gereken, örnek projeyi “mytodoapp-migration:v1” tag’i ile containerize bir hale getirmek.

Containerize Edelim

Todo.DbMigration” projesi içerisinde aşağıdaki gibi bir Dockerfile bulunmaktadır.

FROM mcr.microsoft.com/dotnet/runtime:5.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["Todo.DbMigration/Todo.DbMigration.csproj", "Todo.DbMigration/"]
RUN dotnet restore "Todo.DbMigration/Todo.DbMigration.csproj"
COPY . .
WORKDIR "/src/Todo.DbMigration"
RUN dotnet build "Todo.DbMigration.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Todo.DbMigration.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Todo.DbMigration.dll"]

Şimdi projenin root klasöründe aşağıdaki komutu çalıştırarak migration uygulamasını containerize bir hale getirelim.

docker build -f ./Todo.DbMigration/Dockerfile . -t mytodoapp-migration:v1

Deploy Edelim

Örnek uygulamayı containerize bir hale getirdiğimize göre, deployment işlemini gerçekleştirebiliriz.

Bunun için projenin root klasöründe bulunan “helm-charts” path’ine giderek, aşağıdaki helm komutunu çalıştıralım.

helm upgrade --install --values ./migration-job/values.yaml mytodo-migration ./migration-job

Örnek migration uygulamamız deploy olmuş durumda.

Peki şimdi neler yapabiliriz?

Öncelikle aşağıdaki komutu kullanarak job’ın başarıyla tamamlanıp tamamlanmadığını kontrol edelim.

kubectl get job

Gördüğümüz gibi deploy ettiğimiz migration uygulamasının “COMPLETIONS” durumuna bakarak, başarıyla tamamlanıp tamamlanmadığını anlayabiliriz.

Eğer herhangi bir hata meydana gelseydi, job’ın oluşturduğu ilgili pod’un log’larına bakarak ilgili hatanın izini de sürebilirdik.

Migration sonucunu SQL Server’a bağlanarak kontrol ettiğimizde ise, ilgili migration’ın başarıyla uygulandığını görebiliriz.

Ayrıca bir job’ın tamamlandığında otomatik olarak silinmediğinden de bahsetmiştik. Silme işlemini gerçekleştirmek için ise, aşağıdaki standart helm komutunu kullanabiliriz.

helm delete mytodo-migration

Peki diyelim ki biz bu işlemleri automated bir hale getirmek istiyoruz. Örneğin Azure DevOps kullanıyoruz ve ilgili migration job’ının, başarıyla tamamlanmasından sonra otomatik olarak silinmesini istiyoruz.

Bu işlemi gerçekleştirebilmek için ise, “kubectl” in “wait” komutundan yararlanabiliriz. Bir başka değişle ilgili job’ı silmeden önce, bekleme işlemini job tamamlanana kadar sürdürmemiz gerekmektedir.

Bunu ise aşağıdaki komut yardımı ile yapabiliriz.

kubectl wait --for-condition=complete job/mytodo-migration-migration-job --timeout=2m

Gördüğümüz gibi belirttiğimiz condition gerçekleşene kadar ilgili task, timeout süresi boyunca bekletilecektir. Bu işlemin ardından ise job’ın silinme işlemini gerçekleştirebiliriz.

Özetle

Kubernetes jobs, diğer pod controller’larından farklı olarak bizlere tek seferlik işlemlerimizi çalıştırabileceğimiz bir yapı sağlamaktadır. Bizde bu yapı ile migration gibi işlemlerimizi nasıl gerçekleştirebileceğimize basitçe bakmaya çalıştık.

Eğer migration uygulamasını ana uygulamadan bağımsız olarak deploy etmiyor olsaydık, bir başka değişle ana uygulamamızın deployment anında migration işlemlerini de gerçekleştirmek isteseydik, init container veya helm hook yöntemlerini de kullanım senaryolarına göre tercih edebilirdik.

Bunların yanı sıra job’ların, farklı kullanım senaryoları ve özellikleri de mevcuttur. Örneğin bir job’ın execution time’ını “activeDeadlineSeconds” parametresi ile kontrol edebiliriz. Farklı kullanım senaryoları için job konsept’i hakkında daha fazla bilgi edinebilmek adına, burayı inceleyebilirsiniz.

Gökhan Gökalp

View Comments

  • Thanks for this article. I want use a secret for storing connectionStrings. Unfortunately I can't this get to work. The connectstring isn't read from the secret and I can't figure out what went wrong. I created a generic secret which is working ok in my aspnetcore applications, but not in the configured job.

    Please, can you help me. Thanks, Marcel

    I have changed the job.yaml file like this

    apiVersion: batch/v1
    kind: Job
    metadata:
    name: migrations
    labels:
    app: migrations
    spec:
    backoffLimit: 0
    template:
    spec:
    containers:
    - name: migrations
    image: marcelb/migrations:v13
    imagePullPolicy: Always
    env:
    - name: "ASPNETCORE_ENVIRONMENT"
    value: "staging"
    volumeMounts:
    - name: secrets
    mountPath: /app/secret
    readOnly: true
    volumes:
    - name: secrets
    secret:
    secretName: secret-appsettingsmyfirstblazorapplication
    restartPolicy: Never
    nodeSelector:
    beta.kubernetes.io/os: linux

Recent Posts

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago

Dapr ve .NET Kullanarak Minimum Efor ile Microservice’ler Geliştirmek – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 yıl ago