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.
Ben geliştirme ortamı olarak Docker Desktop’un Kubernetes özelliğini kullanacağı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, 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.
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.
“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
Ö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.
Ö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.
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.
{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…
{: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.…
{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…
{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…
{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…
{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…
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
Hi Marcel,
thanks for your comment. As far as I understand, you want to read your secrets as environment. If my assumption is correct, then you need to have this schema. https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-environment-variables With your current approach, your secrets will appear under the /app/secret folder.