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

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

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

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:

  • Dependency restore ve deterministic build
  • Unit ve integration testleri
  • Statik analiz ve güvenlik taraması
  • Versiyonlanmış artifact oluşturma
  • 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:"code-comment">//+: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:"code-comment">//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ç