← Back to all posts

CI/CD Pipeline — .NET Projesini GitHub Actions ile Otomatik Deploy Etmek

CI/CD Pipeline — .NET Projesini GitHub Actions ile Otomatik Deploy Etmek

CI/CD Pipeline — .NET Projesini GitHub Actions ile Otomatik Deploy Etmek

Neden CI/CD?

Kod yazdınız, test ettiniz, çalışıyor. Şimdi production'a almak için sıra geldi. Eğer hâlâ şu adımları elle yapıyorsanız:

1. Visual Studio'da Release build al
2. FTP veya RDP ile sunucuya bağlan
3. IIS'i durdur
4. Dosyaları kopyala
5. web.config'i kontrol et
6. IIS'i başlat
7. Tarayıcıda test et
8. Bir şeyler yanlışsa geri al — ama nasıl?

Bu süreç hatalıdır. İnsan faktörü devreye girer, adım atlanır, yanlış dosya kopyalanır, gece yarısı deployment stresi yaşanır.

CI/CD bu süreci otomatik, tekrarlanabilir ve güvenilir hale getirir. Kodu push ettiğinizde pipeline devreye girer — build alır, testleri çalıştırır, production'a deploy eder. Siz kahvenizi içersiniz.


Kavramlar — CI ve CD Ayrımı

CI — Continuous Integration (Sürekli Entegrasyon)

Her kod push'unda otomatik olarak:

  • Proje derlenir
  • Unit ve integration testler çalıştırılır
  • Code quality kontrolleri yapılır
  • Sorunlar anında bildirilir

CD — Continuous Delivery / Deployment (Sürekli Teslimat / Dağıtım)

CI başarıyla tamamlandığında otomatik olarak:

  • Artifact paketlenir
  • Staging ortamına deploy edilir
  • (Opsiyonel onay sonrası) Production'a deploy edilir
Kod Push → CI (Build + Test) → CD (Package + Deploy)
    ↓              ↓                      ↓
  GitHub    GitHub Actions          IIS Server

Senaryo — Ne Kuracağız?

Bu yazıda şu pipeline'ı kuracağız:

main branch'e push geldi
         ↓
    Kod checkout edildi
         ↓
    .NET 8 build alındı
         ↓
    Unit testler çalıştırıldı
         ↓
    Publish artifact oluşturuldu
         ↓
    IIS'e deploy edildi
         ↓
    Health check yapıldı

Hedef ortam: Windows Server + IIS (Türkiye'deki .NET projelerinin büyük çoğunluğu)


GitHub Actions Temel Yapısı

GitHub Actions, .github/workflows/ klasöründeki YAML dosyalarıyla tanımlanır. Her dosya bir workflow'dur.

# .github/workflows/deploy.yml

name: Build and Deploy        # Workflow adı

on:                           # Tetikleyici
  push:
    branches: [ main ]        # main'e push gelince çalış
  pull_request:
    branches: [ main ]        # PR açılınca da çalış (sadece CI)

jobs:                         # İşler
  build:                      # İş adı
    runs-on: windows-latest   # Çalışma ortamı

    steps:                    # Adımlar
      - name: Checkout
        uses: actions/checkout@v4

Adım 1 — Proje Yapısını Hazırlayın

MyApp/
├── .github/
│   └── workflows/
│       ├── ci.yml           # PR'larda çalışan CI
│       └── deploy.yml       # main'e merge sonrası deploy
├── src/
│   ├── MyApp.API/
│   ├── MyApp.Application/
│   ├── MyApp.Infrastructure/
│   └── MyApp.Domain/
├── tests/
│   ├── MyApp.UnitTests/
│   └── MyApp.IntegrationTests/
└── MyApp.sln

Adım 2 — CI Workflow (Build + Test)

Her PR açıldığında ve her push'ta çalışır. Kırık kod main'e girmez.

# .github/workflows/ci.yml

name: CI — Build and Test

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest    # Linux runner — daha hızlı ve ücretsiz

    steps:
      - name: Kodu İndir
        uses: actions/checkout@v4

      - name: .NET 8 Kurulumu
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Bağımlılıkları Yükle
        run: dotnet restore MyApp.sln

      - name: Build Al
        run: dotnet build MyApp.sln --no-restore --configuration Release

      - name: Unit Testleri Çalıştır
        run: |
          dotnet test tests/MyApp.UnitTests/MyApp.UnitTests.csproj \
            --no-build \
            --configuration Release \
            --logger "trx;LogFileName=unit-test-results.trx" \
            --collect:"XPlat Code Coverage"

      - name: Integration Testleri Çalıştır
        run: |
          dotnet test tests/MyApp.IntegrationTests/MyApp.IntegrationTests.csproj \
            --no-build \
            --configuration Release \
            --logger "trx;LogFileName=integration-test-results.trx"

      - name: Test Sonuçlarını Yayınla
        uses: dorny/test-reporter@v1
        if: always()          # Test başarısız olsa bile sonuçları göster
        with:
          name: Test Sonuçları
          path: '**/*.trx'
          reporter: dotnet-trx

      - name: Code Coverage Raporu
        uses: codecov/codecov-action@v4
        with:
          files: '**/coverage.cobertura.xml'

Adım 3 — GitHub Secrets Tanımlayın

Deploy workflow'unda sunucu bilgileri kullanılacak. Bunları asla YAML'a yazmayın — GitHub Secrets'a ekleyin.

GitHub Repository → Settings → Secrets and variables → Actions → New repository secret

Eklenecek secret'lar:

IIS_SERVER_HOST        → Sunucu IP veya hostname (örn: 192.168.1.100)
IIS_SERVER_USERNAME    → Deployment kullanıcısı (örn: deploy_user)
IIS_SERVER_PASSWORD    → Deployment kullanıcısı şifresi
IIS_SITE_NAME          → IIS site adı (örn: MyApp)
IIS_APP_POOL_NAME      → Application Pool adı (örn: MyAppPool)
DEPLOY_PATH            → Deploy klasörü (örn: C:\inetpub\wwwroot\MyApp)
HEALTH_CHECK_URL       → Deploy sonrası kontrol URL'i

Adım 4 — Deploy Workflow (IIS)

# .github/workflows/deploy.yml

name: Deploy to IIS

on:
  push:
    branches: [ main ]       # Sadece main'e push'ta deploy et

env:
  DOTNET_VERSION: '8.0.x'
  PROJECT_PATH: 'src/MyApp.API/MyApp.API.csproj'
  PUBLISH_DIR: './publish'

jobs:
  # ── 1. Build ve Test ──────────────────────────────────────────
  build:
    name: Build ve Test
    runs-on: ubuntu-latest

    steps:
      - name: Kodu İndir
        uses: actions/checkout@v4

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

      - name: Bağımlılıkları Yükle
        run: dotnet restore ${{ env.PROJECT_PATH }}

      - name: Build Al
        run: |
          dotnet build ${{ env.PROJECT_PATH }} \
            --no-restore \
            --configuration Release

      - name: Testleri Çalıştır
        run: |
          dotnet test MyApp.sln \
            --no-build \
            --configuration Release \
            --verbosity normal

      - name: Publish
        run: |
          dotnet publish ${{ env.PROJECT_PATH }} \
            --no-build \
            --configuration Release \
            --output ${{ env.PUBLISH_DIR }} \
            --runtime win-x64 \
            --self-contained false

      - name: Artifact Yükle
        uses: actions/upload-artifact@v4
        with:
          name: published-app
          path: ${{ env.PUBLISH_DIR }}
          retention-days: 7    # 7 gün sakla, sonra sil

  # ── 2. Deploy ─────────────────────────────────────────────────
  deploy:
    name: IIS Deploy
    runs-on: windows-latest   # WinRM için Windows runner gerekli
    needs: build              # Build başarılı olursa çalış
    environment: production   # GitHub Environment — onay gerektirilebilir

    steps:
      - name: Artifact İndir
        uses: actions/download-artifact@v4
        with:
          name: published-app
          path: ${{ env.PUBLISH_DIR }}

      - name: WinRM ile IIS'e Deploy Et
        shell: pwsh
        env:
          SERVER_HOST: ${{ secrets.IIS_SERVER_HOST }}
          SERVER_USER: ${{ secrets.IIS_SERVER_USERNAME }}
          SERVER_PASS: ${{ secrets.IIS_SERVER_PASSWORD }}
          SITE_NAME: ${{ secrets.IIS_SITE_NAME }}
          APP_POOL: ${{ secrets.IIS_APP_POOL_NAME }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
        run: |
          # WinRM bağlantısı kur
          $securePass = ConvertTo-SecureString $env:SERVER_PASS -AsPlainText -Force
          $credential = New-Object System.Management.Automation.PSCredential(
            $env:SERVER_USER, $securePass)

          $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck
          $session = New-PSSession `
            -ComputerName $env:SERVER_HOST `
            -Credential $credential `
            -UseSSL `
            -SessionOption $sessionOptions

          # Dosyaları sunucuya kopyala
          Copy-Item -Path "${{ env.PUBLISH_DIR }}\*" `
            -Destination $env:DEPLOY_PATH `
            -ToSession $session `
            -Recurse `
            -Force

          # IIS'te Application Pool'u yeniden başlat
          Invoke-Command -Session $session -ScriptBlock {
            param($appPool, $siteName)

            Import-Module WebAdministration

            # App Pool durdur
            if ((Get-WebAppPoolState -Name $appPool).Value -ne 'Stopped') {
              Stop-WebAppPool -Name $appPool
              Start-Sleep -Seconds 3
            }

            # App Pool başlat
            Start-WebAppPool -Name $appPool
            Start-Sleep -Seconds 2

            Write-Host "Deploy tamamlandı. Site: $siteName | Pool: $appPool"
          } -ArgumentList $env:APP_POOL, $env:SITE_NAME

          Remove-PSSession $session

      - name: Health Check
        shell: pwsh
        run: |
          $url = "${{ secrets.HEALTH_CHECK_URL }}"
          $maxRetries = 5
          $retryCount = 0
          $success = $false

          while ($retryCount -lt $maxRetries -and -not $success) {
            try {
              $response = Invoke-WebRequest -Uri $url -TimeoutSec 10
              if ($response.StatusCode -eq 200) {
                Write-Host "✅ Health check başarılı: $url"
                $success = $true
              }
            } catch {
              $retryCount++
              Write-Host "⏳ Deneme $retryCount/$maxRetries başarısız. 5 saniye bekleniyor..."
              Start-Sleep -Seconds 5
            }
          }

          if (-not $success) {
            Write-Error "❌ Health check $maxRetries denemeden sonra başarısız oldu!"
            exit 1
          }

Adım 5 — Health Check Endpoint

Pipeline'ın son adımında çağrılan health check endpoint'ini uygulamaya ekleyin:

// Program.cs
builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "database",
        tags: ["db", "sql"])
    .AddRedis(
        redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
        name: "redis",
        tags: ["cache"]);

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false   // Sadece uygulama ayakta mı?
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db")
});

NuGet paketi:

dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.Redis

Adım 6 — Environment Koruması

Production ortamına deploy öncesi manuel onay istemek için GitHub Environments kullanın:

GitHub Repository
  → Settings
  → Environments
  → New environment: "production"
  → Required reviewers: [kullanıcı adınız]
  → Wait timer: 0 minutes

Deploy workflow'unda environment tanımlandıysa — environment: production — GitHub onay bekler:

CI başarılı ✅
     ↓
Onay bekleniyor... ⏳
     ↓
[Onayla] veya [Reddet]
     ↓
Deploy başladı 🚀

Gelişmiş — Rollback Stratejisi

Deploy sonrası sorun çıkarsa hızlıca geri almak için önceki artifact'ı saklayın ve rollback workflow'u tanımlayın:

# .github/workflows/rollback.yml

name: Rollback

on:
  workflow_dispatch:           # Elle tetiklenir
    inputs:
      run_id:
        description: 'Geri alınacak başarılı deploy Run ID'
        required: true
        type: string

jobs:
  rollback:
    name: Rollback Deploy
    runs-on: windows-latest
    environment: production

    steps:
      - name: Önceki Artifact'ı İndir
        uses: actions/download-artifact@v4
        with:
          name: published-app
          run-id: ${{ inputs.run_id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          path: ./rollback-publish

      - name: Rollback Deploy
        shell: pwsh
        env:
          SERVER_HOST: ${{ secrets.IIS_SERVER_HOST }}
          SERVER_USER: ${{ secrets.IIS_SERVER_USERNAME }}
          SERVER_PASS: ${{ secrets.IIS_SERVER_PASSWORD }}
          APP_POOL: ${{ secrets.IIS_APP_POOL_NAME }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
        run: |
          $securePass = ConvertTo-SecureString $env:SERVER_PASS -AsPlainText -Force
          $credential = New-Object System.Management.Automation.PSCredential(
            $env:SERVER_USER, $securePass)

          $session = New-PSSession `
            -ComputerName $env:SERVER_HOST `
            -Credential $credential `
            -UseSSL

          Copy-Item -Path ".\rollback-publish\*" `
            -Destination $env:DEPLOY_PATH `
            -ToSession $session `
            -Recurse -Force

          Invoke-Command -Session $session -ScriptBlock {
            param($appPool)
            Import-Module WebAdministration
            Stop-WebAppPool -Name $appPool
            Start-Sleep -Seconds 2
            Start-WebAppPool -Name $appPool
            Write-Host "Rollback tamamlandı."
          } -ArgumentList $env:APP_POOL

          Remove-PSSession $session

      - name: Rollback Bildirimi
        run: |
          echo "⚠️ Rollback tamamlandı. Run ID: ${{ inputs.run_id }}"

Gelişmiş — Slack Bildirimi

Deploy sonucu ekibe bildirilsin:

      - name: Başarı Bildirimi
        if: success()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "text": "✅ *${{ github.repository }}* production'a deploy edildi.\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Deploy Eden:* ${{ github.actor }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

      - name: Hata Bildirimi
        if: failure()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "text": "❌ *${{ github.repository }}* deploy başarısız!\n*Branch:* ${{ github.ref_name }}\n*Hata:* Pipeline başarısız oldu. Loglara bakın."
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Sunucu Hazırlığı — WinRM Yapılandırması

GitHub Actions'ın sunucuya bağlanabilmesi için WinRM'i yapılandırın:

# Sunucuda PowerShell (Administrator) ile çalıştırın

# WinRM'i etkinleştir
Enable-PSRemoting -Force

# HTTPS için self-signed sertifika oluştur
$cert = New-SelfSignedCertificate `
  -DnsName $env:COMPUTERNAME `
  -CertStoreLocation Cert:\LocalMachine\My

# WinRM HTTPS listener ekle
New-Item -Path WSMan:\LocalHost\Listener `
  -Transport HTTPS `
  -Address * `
  -CertificateThumbprint $cert.Thumbprint `
  -Force

# Firewall kuralı ekle
New-NetFirewallRule `
  -DisplayName "WinRM HTTPS" `
  -Direction Inbound `
  -Protocol TCP `
  -LocalPort 5986 `
  -Action Allow

# Deploy kullanıcısı oluştur — minimum yetki
$password = ConvertTo-SecureString "GüvenliŞifre123!" -AsPlainText -Force
New-LocalUser -Name "deploy_user" -Password $password -PasswordNeverExpires
Add-LocalGroupMember -Group "Remote Management Users" -Member "deploy_user"

# Deploy klasörüne yazma yetkisi ver
$acl = Get-Acl "C:\inetpub\wwwroot\MyApp"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
  "deploy_user", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($rule)
Set-Acl "C:\inetpub\wwwroot\MyApp" $acl

Denetim Listesi

✅ .github/workflows/ klasörü oluşturuldu
✅ CI workflow — her PR'da build ve test çalışıyor
✅ Deploy workflow — sadece main'e push'ta tetikleniyor
✅ Sunucu bilgileri GitHub Secrets'a eklendi, YAML'a yazılmadı
✅ Production environment — manuel onay zorunlu
✅ Health check endpoint uygulamaya eklendi
✅ Rollback workflow tanımlandı
✅ Sunucuda WinRM HTTPS yapılandırıldı
✅ Deploy kullanıcısı minimum yetkiyle oluşturuldu
✅ Slack veya e-posta bildirimi yapılandırıldı
✅ Artifact retention süresi belirlendi

Temel Çıkarımlar

  • CI/CD bir lüks değil, profesyonel yazılım geliştirmenin temelidir
  • Her push'ta otomatik test çalıştırmak kırık kodu production'dan uzak tutar
  • Secrets asla YAML'a yazılmaz — GitHub Secrets ya da Vault kullanılır
  • Production deploy'u manuel onay adımıyla koruyun
  • Health check olmayan bir deploy pipeline'ı yarım kalmış demektir
  • Rollback planı deploy planı kadar önemlidir — her zaman geri dönüş yolu hazır olmalıdır

CI/CD'nin amacı hızlı deploy etmek değildir. Güvenli, tekrarlanabilir ve geri alınabilir deploy etmektir. Hız bunun doğal sonucudur.