Tunceli merkezli niş e-ticaret şirketi (organik kuru gıda + bal + bitki çayı), “ay başı satış kampanyası deploy edemiyorduk, 1 hafta önce dondurma şartı koyuyorduk” sorunundan kurtulmak için 7 ay DevOps transformation yaşadı. Sonuç: aylık 8 deploy → günlük 4-6, lead time 2 hafta → 2 saat. Bu yazı sahadaki notlar.
DevOps Nedir?
Development + Operations’un birleştirilmesi. Hedef: yazılımı hızlı + güvenilir + kaliteli üretmek. Sadece araç değil — kültür + süreç + araç birlikte.
| Konu |
Geleneksel |
DevOps |
| Deploy frequency |
Aylık – quarterly |
Günlük – haftalık |
| Lead time (commit → prod) |
Hafta – ay |
Saat – gün |
| MTTR (recovery) |
Saatler – günler |
Dakikalar – saatler |
| Change failure rate |
%~~30+ |
%~~5-10 |
| Dev-Ops ilişkisi |
Silo, “atın duvarın öbür yanına” |
Tek ekip, paylaşılan sorumluluk |
Azure DevOps Servisleri
| Servis |
Hedef |
| Azure Boards |
İş takibi (sprint, kanban, work item) |
| Azure Repos |
Git repository |
| Azure Pipelines |
CI/CD pipeline (multi-platform) |
| Azure Artifacts |
Package management (NuGet, npm, Maven) |
| Azure Test Plans |
Manuel + otomatik test yönetim |
CI/CD Pipeline (E-Ticaret App)
# azure-pipelines.yml
trigger:
branches: { include: [main, release/*] }
paths: { exclude: [docs/**, README.md] }
pool: { vmImage: ubuntu-latest }
variables:
buildConfiguration: 'Release'
stages:
- stage: Build
jobs:
- job: BuildAndTest
steps:
- task: UseDotNet@2
inputs: { version: '8.x' }
- script: dotnet restore
displayName: 'Restore'
- script: dotnet build --configuration $(buildConfiguration) --no-restore
displayName: 'Build'
- script: dotnet test --no-build --logger trx --collect "Code coverage"
displayName: 'Unit tests'
- task: PublishTestResults@2
inputs: { testResultsFormat: VSTest, testResultsFiles: '**/*.trx' }
- script: |
dotnet publish src/Web -c Release -o $(Build.ArtifactStagingDirectory)/web
displayName: 'Publish'
- task: Docker@2
inputs:
command: buildAndPush
repository: ecommerce/web
dockerfile: src/Web/Dockerfile
tags: |
$(Build.BuildId)
latest
containerRegistry: 'acr-prod'
- stage: SecurityScan
jobs:
- job: Trivy
steps:
- script: |
trivy image --severity HIGH,CRITICAL --exit-code 1
$(ACR_NAME).azurecr.io/ecommerce/web:$(Build.BuildId)
displayName: 'Container CVE scan'
- script: |
sonar-scanner -Dsonar.host.url=$(SONAR_URL)
-Dsonar.login=$(SONAR_TOKEN)
displayName: 'SonarQube'
- stage: DeployDev
dependsOn: SecurityScan
jobs:
- deployment: DeployToDev
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'sc-dev'
scriptLocation: inlineScript
inlineScript: |
az containerapp update
--name ecommerce-web-dev
--resource-group rg-ecom-dev
--image $(ACR_NAME).azurecr.io/ecommerce/web:$(Build.BuildId)
- stage: DeployProd
dependsOn: DeployDev
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: BlueGreen
environment: 'production' # manual approval gate
strategy:
runOnce:
deploy:
steps:
- script: |
# Yeni revision deploy (Container Apps, traffic %0)
az containerapp update
--name ecommerce-web-prod
--image $(ACR_NAME).azurecr.io/ecommerce/web:$(Build.BuildId)
--revision-suffix v$(Build.BuildId)
# Smoke test new revision (health endpoint)
SLEEP_PROBE=20
# ... probe checks ...
# Traffic shift: %10 → %50 → %100
az containerapp ingress traffic set
--name ecommerce-web-prod
--revision-weight v$(Build.BuildId)=100 latest=0
Infrastructure as Code (Bicep)
// infra/main.bicep
param location string = resourceGroup().location
param environment string
param appName string
var resourcePrefix = '${appName}-${environment}'
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: '${resourcePrefix}-web'
location: location
properties: {
managedEnvironmentId: containerAppEnv.id
configuration: {
ingress: {
external: true
targetPort: 8080
}
registries: [{
server: '${acrName}.azurecr.io'
identity: managedIdentity.id
}]
}
template: {
containers: [{
name: 'web'
image: '${acrName}.azurecr.io/ecommerce/web:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}]
scale: {
minReplicas: 2
maxReplicas: 20
rules: [{
name: 'http'
http: {
metadata: { concurrentRequests: '50' }
}
}]
}
}
}
}
GitOps (Argo CD / Flux)
Pattern: K8s YAML/Helm chart Git repo'da
Production cluster GitOps tool (Flux) Git'i izler
Git'e merge = otomatik deploy
Avantaj:
- Audit trail (Git log = deploy log)
- Rollback = git revert
- Multi-cluster sync
- "Source of truth" Git
DORA Metrics
| Metrik |
Önce |
Sonra |
Elite hedef |
| Deployment frequency |
~~aylık 8 |
~~günlük 4-6 |
Multiple per day |
| Lead time |
~~2 hafta |
~~2 saat |
<1 hour |
| MTTR |
~~6 saat |
~~22 dk |
<1 hour |
| Change failure rate |
~~%~~28 |
~~%~~6 |
<15% |
Feature Flag (LaunchDarkly / Azure App Config)
Yeni özellik production'a deploy → flag OFF
- Kod canlı ama davranış aktif değil
QA flag ON for test users → test
- Sadece "qa-team" group flag ON
Beta flag ON for %5 user
- Random %5 sample
- A/B test (yeni vs eski)
%100 → herkesin
- Successful → flag kaldır (cleanup)
Kültürel Dönüşüm
| Eski |
Yeni |
| Dev kod yazar, Ops deploy eder |
Tek ekip her şey |
| “Çalışıyor benim makinemde” |
“Pipeline’da çalışıyor” |
| Manuel test (haftalar) |
Otomatik test (dakikalar) |
| Deploy = stres |
Deploy = sıradan |
| Hata = suçlama |
Hata = öğrenme (blameless postmortem) |
Sahada Düşülen Üç Tuzak
- Sadece araç almak, kültür değiştirmemek: Azure DevOps kuruldu ama Ops “deploy gece yapılır” demeye devam ediyor → DevOps yok.
- Test otomasyonunu sona bırakmak: CI/CD var ama manuel test → bottleneck. Test otomasyonu day 1.
- DORA metric’lerini ölçmemek: “İyileştik” der ama veri yok. Ölçmediğin şeyi yönetemezsin.
CloudSpark olarak Azure DevOps + GitHub Actions CI/CD pipeline tasarımı, IaC (Bicep/Terraform), GitOps (Flux/Argo CD), DORA metrics ölçümü ve DevOps kültürel transformation için danışmanlık veriyoruz.