.NET CI/CD: GitHub Actions ve Azure DevOps ile Otomatik Deployment
# .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:
Pipeline Aşamaları
Sağlam bir .NET CI/CD pipeline'ı şu aşamaları sırasıyla takip eder:
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:
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: ./publishBurada 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.
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.
# 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:
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:
- 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 productionSlot 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:
# 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: 10Pipeline'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:
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: 5Readiness 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ı
- 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=moderateTrivy ile Container Image Taraması
- 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ı
- name: Kodda secret taraması yap
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verifiedPipeline'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
# Kötü -- herhangi bir gün uyarısız bozulabilir
uses: actions/checkout@main
# İyi -- belirli bir versiyona sabitlenmiş
uses: actions/checkout@v4Sabitlenmemiş 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:
- 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:
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:
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
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET Testing: Unit, Integration ve E2E Test Stratejileri
.NET projelerinde test stratejisi oluşturun. xUnit, Moq, integration testing ve test piramidi.
.NET ile Mikroservis Mimarisi: Tasarım ve Uygulama
.NET ile mikroservis mimarisi tasarlayın. Service communication, Docker ve orchestration stratejileri.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç