.NET Core

.NET CI/CD: GitHub Actions ve Azure DevOps ile Otomatik Deployment

14 dk okuma9 Şubat 2026Güncellendi 9 Mar 2026
.NET CI/CDGitHub Actions .NETAzure DevOps .NET.NET deploymentPipeline .NETAutomated deployment C#Docker .NET CIRelease automation

Otomatik deployment, hızlı ve güvenli release'lerin anahtarıdır. Manuel süreçlerin getirdiği insan hatalarını ortadan kaldırır, tekrarlanabilir bir yapı sunar ve ekibin asıl işine odaklanmasını sağlar. Bu yazıda, bir .NET projesi için uçtan uca CI/CD pipeline tasarımını gerçek konfigürasyon örnekleriyle ele alıyorum.

CI/CD Neden Bu Kadar Önemli?

Manuel deployment süreçlerinde ekiplerin saatlerce "neden çalışmıyor" diye araştırdığını çok gördüm. Sorun genellikle basit: bir ortam değişkeni eksik, build sırası yanlış ya da test atlanmış. CI/CD bu tür sorunları kökünden çözer.

İyi tasarlanmış bir pipeline size şunları kazandırır:

  • Tekrarlanabilirlik -- her build aynı temiz ortamda çalışır
  • Hız -- otomatik adımlar paralel çalışarak release süresini saatlerden dakikalara indirir
  • Güven -- her commit'te testler ve taramalar çalışır, sorunlar production'a ulaşmadan yakalanır
  • İzlenebilirlik -- her deployment belirli bir commit ve pipeline çalışmasına kadar takip edilebilir

Pipeline Aşamaları

Sağlam bir .NET CI/CD pipeline'ı şu aşamaları sırasıyla takip eder:

  1. Dependency restore ve deterministic build
  2. Unit ve integration testleri
  3. Statik analiz ve güvenlik taraması
  4. Versiyonlanmış artifact oluşturma
  5. Rollback stratejisiyle kontrollü deployment

Her aşamanın pratikte nasıl göründüğüne bakalım.

GitHub Actions ile .NET Workflow

GitHub Actions, .NET projeleri için en sık tercih ettiğim araç. YAML tabanlı konfigürasyonu reponun içinde yaşadığı için versiyon kontrolü altında ve kod gibi review edilebilir durumda.

Build, test, publish ve deployment'ı kapsayan tam bir workflow:

yaml
name: .NET CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  DOTNET_VERSION: '8.0.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: .NET SDK Kur
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Bağımlılıkları yükle
        run: dotnet restore --locked-mode

      - name: Build
        run: dotnet build --no-restore --configuration Release

      - name: Unit testleri çalıştır
        run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults/

      - name: Test sonuçlarını yükle
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: TestResults/

      - name: Publish
        run: dotnet publish src/MyApp/MyApp.csproj --no-build --configuration Release --output ./publish

      - name: Build artifact yükle
        uses: actions/upload-artifact@v4
        with:
          name: app-artifact
          path: ./publish

  deploy-staging:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - name: Artifact indir
        uses: actions/download-artifact@v4
        with:
          name: app-artifact
          path: ./publish

      - name: Azure Web App'e deploy et (Staging)
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-staging
          publish-profile: ${{ secrets.AZURE_STAGING_PUBLISH_PROFILE }}
          package: ./publish

  deploy-production:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - name: Artifact indir
        uses: actions/download-artifact@v4
        with:
          name: app-artifact
          path: ./publish

      - name: Azure Web App'e deploy et (Production)
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-production
          publish-profile: ${{ secrets.AZURE_PROD_PUBLISH_PROFILE }}
          package: ./publish

Burada birkaç önemli detay var. --locked-mode bayrağı, packages.lock.json dosyasının dikkate alınmasını sağlar -- pipeline sırasında beklenmedik paket versiyon değişiklikleri olmaz. Test sonuçlarındaki if: always() ise testler başarısız olsa bile raporları almanızı sağlar; hata ayıklama için vazgeçilmez.

Azure DevOps Pipeline

Microsoft ekositemine yatırım yapmış organizasyonlar için Azure DevOps doğal bir seçim. Azure servisleriyle derin entegrasyon, yerleşik onay kapıları ve detaylı erişim kontrolleri sunar.

yaml
trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '8.0.x'

stages:
  - stage: Build
    jobs:
      - job: BuildAndTest
        steps:
          - task: UseDotNet@2
            inputs:
              packageType: 'sdk'
              version: $(dotnetVersion)

          - script: dotnet restore --locked-mode
            displayName: 'Bağımlılıkları yükle'

          - script: dotnet build --configuration $(buildConfiguration) --no-restore
            displayName: 'Çözümü derle'

          - script: dotnet test --configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"
            displayName: 'Testleri coverage ile çalıştır'

          - task: PublishCodeCoverageResults@2
            inputs:
              summaryFileLocation: '**/coverage.cobertura.xml'

          - script: dotnet publish src/MyApp/MyApp.csproj --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)
            displayName: 'Uygulamayı yayınla'

          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: $(Build.ArtifactStagingDirectory)
              artifactName: 'app-drop'

  - stage: DeployStaging
    dependsOn: Build
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
    jobs:
      - deployment: DeployToStaging
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'MyAzureServiceConnection'
                    appName: 'myapp-staging'
                    package: '$(Pipeline.Workspace)/app-drop'

Deployment pipeline'larımda staging ile production arasında Azure DevOps'un onay kapılarını her zaman kuruyorum. Yerleşik environment approval özelliği, özel scriptlerden çok daha güvenilir. Üstelik kimin neyi ne zaman onayladığının temiz bir kaydını tutar.

.NET için Docker Multi-Stage Build

Konteynerizasyon, günümüzde .NET deployment'larının standardı. Multi-stage Dockerfile, SDK'yı (build araçları) runtime'dan ayırarak image'lerinizi küçük ve build sürecinizi temiz tutar.

dockerfile
# Aşama 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Daha iyi layer cache'leme için önce proje dosyalarını kopyala
COPY *.sln .
COPY src/MyApp/MyApp.csproj src/MyApp/
COPY src/MyApp.Domain/MyApp.Domain.csproj src/MyApp.Domain/
COPY tests/MyApp.Tests/MyApp.Tests.csproj tests/MyApp.Tests/
RUN dotnet restore

# Geri kalanı kopyala ve derle
COPY . .
RUN dotnet build --configuration Release --no-restore

# Aşama 2: Test
FROM build AS test
RUN dotnet test --configuration Release --no-build --logger trx --results-directory /TestResults
# Testler başarısız olursa build burada durur

# Aşama 3: Publish
FROM build AS publish
RUN dotnet publish src/MyApp/MyApp.csproj --configuration Release --no-build --output /app/publish /p:UseAppHost=false

# Aşama 4: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# Root olmayan kullanıcı oluştur
RUN adduser --disabled-password --gecos "" appuser
USER appuser

COPY --from=publish /app/publish .

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

ENTRYPOINT ["dotnet", "MyApp.dll"]

Buradaki kilit optimizasyon, .csproj dosyalarını kopyalayıp kaynak kodun geri kalanından önce dotnet restore çalıştırmak. Docker her katmanı cache'ler, dolayısıyla proje dosyaları değişmediyse restore adımı tamamen atlanır. Pipeline'larımda bu tek numara build sürelerini ortalama %40-60 kısalttı.

Son aşamada root olmayan kullanıcıyla çalışmak güvenlik açısından en iyi pratik. Uygulama ele geçirilirse etki alanını sınırlar.

Docker Build'i CI'a Entegre Etmek

GitHub Actions workflow'unuza Docker build ve push adımını ekleyin:

yaml
  build-and-push-image:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Container Registry'ye giriş yap
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker image build ve push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

Image'lerinizi latest'in yanı sıra her zaman commit SHA'sıyla da etiketleyin. Bu, rollback'leri önemsiz kılar -- önceki SHA etiketini yeniden deploy edersiniz, olur biter.

Deployment Stratejileri

Doğru deployment stratejisini seçmek, uygulamanızın kesinti toleransına ve release'in risk seviyesine bağlıdır.

Blue/Green Deployment

Blue/green, iki özdeş ortam kullanır. Biri (blue) canlı trafiği karşılarken diğeri (green) yeni deployment'ı alır. Doğrulama sonrası trafik green'e yönlendirilir.

Azure App Service'te deployment slot'ları sayesinde bunu hazır olarak alırsınız:

yaml
  - name: Staging slot'a deploy et
    uses: azure/webapps-deploy@v3
    with:
      app-name: myapp-production
      slot-name: staging
      package: ./publish

  - name: Staging slot'a smoke test çalıştır
    run: |
      response=$(curl -s -o /dev/null -w "%{http_code}" https://myapp-production-staging.azurewebsites.net/health)
      if [ "$response" != "200" ]; then
        echo "Smoke test başarısız, status: $response"
        exit 1
      fi

  - name: Slot'ları değiştir
    uses: azure/cli@v2
    with:
      inlineScript: |
        az webapp deployment slot swap \
          --resource-group myapp-rg \
          --name myapp-production \
          --slot staging \
          --target-slot production

Slot swap'ın güzelliği şu: bir şeyler ters giderse sadece geri swap yaparsınız. Önceki versiyon hala staging slot'unda çalışıyor durumda.

Canary Deployment

Canary release'ler, trafiğin küçük bir yüzdesini yeni versiyona yönlendirir ve güven arttıkça bu oranı kademeli olarak yükseltir. Kısa süreli bir kesintinin bile kabul edilemez olduğu yüksek trafikli servisler için idealdir.

Azure Front Door veya Istio gibi bir service mesh ile trafik bölme yapılandırabilirsiniz:

yaml
# Istio VirtualService ile Kubernetes canary
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
    - myapp.example.com
  http:
    - route:
        - destination:
            host: myapp-stable
            port:
              number: 80
          weight: 90
        - destination:
            host: myapp-canary
            port:
              number: 80
          weight: 10

Pipeline'larımda canary rollout'ları %5 trafikle başlatıyorum ve en az 15 dakika hata oranlarını ve gecikmeleri izledikten sonra yükseltiyorum. Herhangi bir anahtar metrik kötüleşirse pipeline otomatik olarak geri alıyor.

Rolling Deployment

Rolling deployment'lar instance'ları teker teker günceller. Kubernetes'te bu varsayılan stratejidir:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    spec:
      containers:
        - name: myapp
          image: ghcr.io/myorg/myapp:latest
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5

Readiness probe burada kritik. Olmazsa Kubernetes, henüz ayağa kalkmakta olan bir pod'a trafik yönlendirebilir.

CI'da Güvenlik Taraması

Güvenlik sonradan eklenen bir şey olamaz. Taramaları pipeline'a entegre etmek, açıkları production'a ulaşmadan yakalar.

Bağımlılık Güvenlik Açığı Taraması

yaml
  - name: Bilinen güvenlik açıklarını kontrol et
    run: dotnet list package --vulnerable --include-transitive

  - name: NuGet Audit çalıştır
    run: dotnet restore --force-evaluate /p:NuGetAudit=true /p:NuGetAuditLevel=moderate

Trivy ile Container Image Taraması

yaml
  - name: Docker image güvenlik açığı taraması
    uses: aquasecurity/trivy-action@master
    with:
      image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
      format: 'sarif'
      output: 'trivy-results.sarif'
      severity: 'CRITICAL,HIGH'

  - name: Trivy tarama sonuçlarını yükle
    uses: github/codeql-action/upload-sarif@v3
    with:
      sarif_file: 'trivy-results.sarif'

Secret Taraması

yaml
  - name: Kodda secret taraması yap
    uses: trufflesecurity/trufflehog@main
    with:
      extra_args: --only-verified

Pipeline'larımda güvenlik taraması hatalarını kesin engelleyici olarak ele alıyorum. Bilinen kritik güvenlik açıklarının production'a geçmesine izin veren bir pipeline, hiç pipeline olmamasından daha kötüdür -- sahte bir güvenlik hissi verir.

Sık Yapılan CI/CD Hataları

Yıllar içinde aynı hataların ekipler arasında tekrarlandığını gördüm. En çok zarar verenleri şunlar:

1. Action ve Image Versiyonlarını Sabitlememek

yaml
# Kötü -- herhangi bir gün uyarısız bozulabilir
uses: actions/checkout@main

# İyi -- belirli bir versiyona sabitlenmiş
uses: actions/checkout@v4

Sabitlenmemiş versiyonlar, bir upstream action breaking change yayınladığı için pipeline'ınızın rastgele bir salı günü bozulabileceği anlamına gelir. Her zaman versiyonları sabitleyin.

2. Secret'ları Repoya Koymak

Kulağa bariz geliyor ama hala connection string'lerin ve API key'lerin commit'lendiği repolarla karşılaşıyorum. CI platformunuzun secret yönetimini kullanın. GitHub Actions'ta bu repository veya environment secret'ları demek. Azure DevOps'ta ise Azure Key Vault entegrasyonu.

3. "Zaman Kazanmak" İçin Testleri Atlamak

Bazı ekipler yavaş oldukları için pipeline'daki testleri devre dışı bırakıyor. Doğru çözüm testleri hızlandırmaktır -- paralelleştirin, unit testleri integration testlerinden ayırın ya da test impact analysis kullanın. Asla atlamayın.

4. Rollback Planı Olmaması

Her deployment'ın test edilmiş bir rollback mekanizması olmalı. İster slot swap, ister Kubernetes rollback, ister önceki artifact'ın yeniden deploy'u olsun -- rollback yolu otomatik olmalı ve düzenli olarak denenmelidir.

5. Pipeline Performansını Göz Ardı Etmek

45 dakika süren bir pipeline geliştirici verimliliğini öldürür. Pipeline çalışma sürenizi takip edin ve agresif bir şekilde optimize edin. Yaygın kazanımlar:

  • NuGet paketlerini çalışmalar arası cache'leme
  • Bağımsız job'ları paralel çalıştırma
  • Mümkün olduğunda incremental build kullanma
  • Doğru runner boyutunu seçme
yaml
  - name: NuGet paketlerini cache'le
    uses: actions/cache@v4
    with:
      path: ~/.nuget/packages
      key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
      restore-keys: |
        ${{ runner.os }}-nuget-

6. Her Branch İçin Aynı Pipeline

Feature branch'lerin staging'e deploy edilmesine gerek yok. Sadece ilgili aşamaları çalıştırmak için koşullu mantık kullanın:

yaml
deploy-staging:
  if: github.ref == 'refs/heads/develop'

deploy-production:
  if: github.ref == 'refs/heads/main'

Operasyonel Göstergeler

Pipeline'ın ötesinde, release sürecinize dair gözlemlenebilirliğe ihtiyacınız var:

  • Deployment sıklığı -- ne sıklıkla deploy ediyorsunuz. Genellikle yüksek olan iyidir, ancak yalnızca hata oranınız düşük kalırsa.
  • Değişiklik hata oranı -- deployment'ların yüzde kaçı sorun çıkarıyor. Hedef %5'in altı.
  • Ortalama kurtarma süresi (MTTR) -- başarısız bir deployment'tan ne kadar hızlı kurtuluyorsunuz. Otomatik rollback'ler bunu dramatik şekilde düşürür.
  • Değişiklik lead time -- commit'ten production'a kadar geçen süre. Sağlıklı bir pipeline bunu bir saatin altında tutar.

Bu metrikleri takip edin ve aylık gözden geçirin. Ekibinizin teslimat sağlığı hakkında tek bir araç veya konfigürasyondan daha fazla şey söylerler.

Sonuç

Etkili CI/CD, hem teknik otomasyon hem de release yönetişimidir. İyi kurulmuş bir pipeline sadece komut çalıştırmaz -- kalite kapılarını zorlar, güvenli deployment'lar sağlar ve tüm ekibe her release'in production'a hazır olduğu güvenini verir. Doğru şekilde kurmanın yatırımı, her bir deployment'ta karşılığını verir.

CI/CD pipeline'ınızı birlikte tasarlayabiliriz.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç