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.