Containerization Sahada: Bir Geleneksel Şirketin Monolit .NET Framework 4.8 Uygulamasının 6 Aylık Container'a Geçiş Yolculuğu

Gümüşhane merkezli, 14 yıllık üretim ERP’si olan 65 çalışanlı bir maden ekipmanı şirketi, “Windows Server 2012 R2 EOL geldi, sunucu çöküyor, biz bu uygulamayı taşımak zorundayız” gerçeğiyle uygulamayı .NET Framework 4.8’den .NET 8 + Linux container’a 6 ayda taşıdı. Bu yazı sahadaki notlar.

Container Nedir? Avantajları

Avantaj Açıklama
Portability “Çalıştığı yerde çalışır” — local, test, prod
Density VM’den 10x daha çok container 1 host’ta
Hızlı startup Saniyeler (VM dakikalar)
Immutable infra Container = artifact, modify değil replace
Microservice friendly Bağımsız scale, deploy
Resource isolation cgroup + namespace

Geleneksel App’i Container’a Almak: Yollar

Yaklaşım Açıklama Bu vaka
Lift & Shift to Windows Container .NET Framework 4.x Windows container’da çalışır Önce denendi (büyük image, ağır)
Migrate to .NET 8 + Linux container Modernize + küçük image + ucuz ✓ Seçildi
Refactor + microservice Tam yeniden tasarla — (bütçe yok)

Discovery: Bağımlılık Analizi

.NET Framework 4.8 monolit:
  - 280 .csproj
  - ~~620K LOC
  - 18 dış DLL referansı
  - SQL Server (intel CLR functions)
  - Crystal Reports (Windows-only!)
  - 4 Windows Service

.NET 8 uyumluluk araç: .NET Upgrade Assistant
  - Compatible: %~~78 paket
  - Replacement gerek: 12 paket (3rd party API yok)
  - Block: 3 paket (Crystal Reports, Windows-only DLL, COM interop)

Karar: 
  - Crystal Reports → web tabanlı raporlama (FastReports .NET 8)
  - Windows Service → Worker Service (.NET 8 cross-platform)
  - COM interop → REST API wrapper (eski Windows VM'de canlı)

Migration Yol Haritası

Hafta Aşama
1-2 Discovery + .NET Upgrade Assistant rapor
3-6 Engelleyici dependency’leri çöz (Crystal alternatif, vs)
7-12 Code migration (ProjectName.csproj sdk-style)
13-16 Compile + unit test (Linux’a)
17-20 Dockerfile + multi-stage
21-22 Local Docker test
23-24 AKS deploy + smoke test
25-26 Paralel canlı + cutover

Code Migration Örnekleri

Eski (.NET Framework 4.8)

// HttpClient yok, WebClient kullanım
using (var client = new WebClient())
{
    var data = client.DownloadString(url);
}

// System.Configuration.ConfigurationManager
var conn = ConfigurationManager.ConnectionStrings["Default"].ConnectionString;

Yeni (.NET 8)

// HttpClient + IHttpClientFactory (DI)
public class MyService(IHttpClientFactory http)
{
    public async Task GetData(string url)
    {
        var client = http.CreateClient();
        return await client.GetStringAsync(url);
    }
}

// IConfiguration (Microsoft.Extensions.Configuration)
public class MyService(IConfiguration config)
{
    private readonly string conn = config.GetConnectionString("Default");
}

Dockerfile (Multi-Stage)

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.sln .
COPY src/*/*.csproj ./
RUN for file in *.csproj; do mkdir -p src/${file%.csproj} && mv $file src/${file%.csproj}/; done
RUN dotnet restore

COPY . .
RUN dotnet publish src/Erp.Web -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
RUN groupadd -r erp && useradd -r -g erp erp
USER erp
COPY --from=build --chown=erp:erp /app/publish .

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

HEALTHCHECK --interval=30s --timeout=5s 
    CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1

ENTRYPOINT ["dotnet", "Erp.Web.dll"]

Image boyutu: 2.4 GB (Windows container) → 220 MB (Linux .NET 8) — %~~91 küçülme.

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: erp-web
  namespace: erp
spec:
  replicas: 2
  selector: { matchLabels: { app: erp-web } }
  template:
    metadata: { labels: { app: erp-web } }
    spec:
      containers:
      - name: erp
        image: acr.azurecr.io/erp-web:1.0
        ports: [{ containerPort: 8080 }]
        resources:
          requests: { cpu: "200m", memory: "256Mi" }
          limits:   { cpu: "1500m", memory: "1Gi" }
        readinessProbe:
          httpGet: { path: /health/ready, port: 8080 }
          initialDelaySeconds: 5
        livenessProbe:
          httpGet: { path: /health/live, port: 8080 }
          initialDelaySeconds: 30
        env:
        - name: ConnectionStrings__Default
          valueFrom:
            secretKeyRef: { name: erp-secrets, key: db-conn }

Sonuçlar (6 Ay)

Metrik Önce Sonra
Sunucu sayısı 4 Windows Server (2012 R2) 1 AKS cluster (3 node)
Image boyutu 220 MB (Linux container)
Deploy süresi ~~~~3 saat (manuel) ~~5 dk (CI/CD)
App startup ~~90 sn ~~12 sn
RAM kullanım (avg) ~~~~6.8 GB ~~~~520 MB
Aylık compute fatura ~~$1.800 (4 server) ~~$640 (AKS shared)
Patch yönetim ~~aylık manuel ~~yıllık (managed K8s)

Sahada Düşülen Üç Tuzak

  1. Windows Container’ı son hedef sanmak: Windows container çalışır ama image büyük + lisans pahalı. Linux .NET 8’e geçiş ek emek ama uzun vadede karlı.
  2. Test coverage olmadan refactor: Unit test yazmadan modernize → regression bulutu. Önce test yaz, sonra modernize.
  3. Container = sihirli kutu sanmak: Health probe + resource limit + secret management eksikse production’da kâbus.

CloudSpark olarak .NET Framework → .NET 8 modernization, Linux container migration, Dockerfile multi-stage optimization, AKS deployment ve GitOps CI/CD için danışmanlık veriyoruz.

Etiketler:
🇹🇷 Türkçe🇬🇧 English🇩🇪 Deutsch🇫🇷 Français🇸🇦 العربية🇷🇺 Русский🇪🇸 Español