Mert Özen Her Satırda Daha İleri
DevOps Azure DevOps Pipelines — #06

Environment, Approval ve Check'ler: Deployment Job ile YAML'da Release Süreci

Mert Özen 15 Kas 2026 11 dk 7 görüntülenme
Environment, Approval ve Check'ler: Deployment Job ile YAML'da Release Süreci

Azure Pipelines'ta approval ve check'ler YAML'da değil, environment gibi resource'ların üzerinde tanımlanır; bu yüzden pipeline dosyasını değiştiren biri onları atlayamaz. Bookstore'a deployment job'lı dev, staging ve production stage'leri ekliyor, release artifact'ını doğruluyor, check'lerin sırasını, production'a yalnızca main'den onayla gitmeyi ve classic release karşılıklarını görüyoruz.

Sürüm notu: Bu yazıdaki release doğrulama script'i ve onun girdisi olan API ve Angular çıktısı .NET 10 (SDK 10.0.404), Angular 22 ve Node.js 24 ile yerelde denenmiştir; pipeline ve template dosyaları yq 4.54 ile ayrıştırılarak denetlenmiştir. Environment'ların, deployment job'ların, approval ve check'lerin Azure Pipelines içindeki davranışı için Microsoft Learn'deki resmî belgelerden (Kasım 2026 sürümleri) yararlanılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) pipeline'ına bu yazıda üç deploy stage'i ekliyoruz: dev, staging ve production. Production stage'i ortak template'ten geliyor, yalnızca main'den çalışacak şekilde bir condition taşıyor ve önünde Selin'in onayı var. Ayşe feature/fast-checkout branch'inde acil bir düzeltme yapıyor ve beklemek istemiyor: branch'indeki azure-pipelines.yml'da ortak iskeleti bırakıp kendi yazdığı, condition'sız bir production stage'i koyuyor ve run'ı elle başlatıyor. Branch'inin kodu onaysız production'a gider mi?

Gitmez, ve bunun nedeni YAML (girintiyle yapı kuran, insanın okuyabileceği veri formatı) dosyasının içinde değil. Onay, branch kısıtı ve diğer kontroller pipeline dosyasında tanımlanmıyor; production environment'ının sahipleri tarafından web arayüzünde, o resource'un üzerinde tanımlanıyor. Belgelerin vurguladığı nokta bu: YAML dosyasını değiştiren kişi, bir stage başlamadan önce çalışacak check'leri değiştiremiyor. Ayşe'nin stage'i bookstore-production environment'ını kullandığı anda Azure Pipelines duruyor ve o environment'taki bütün check'lerin geçmesini bekliyor. Branch control check'i refs/heads/main dışındaki her branch'i, required template check'i de ortak iskeletten extends etmeyen her pipeline'ı reddediyor; ikisi de approval'dan önce çalıştığı için stage, Selin'e sıra gelmeden başarısız oluyor. YAML'daki condition pipeline yazarının niyetini anlatıyor; check ise resource sahibinin kuralını uyguluyor.

Environment Resource'u ve Deployment History

Environment, Azure Pipelines'ta deploy hedefini temsil eden bir resource. İçine Kubernetes namespace'leri ya da sanal makineler eklenebiliyor, ama boş da kullanılabiliyor; belgeler bu durumda environment'ın deploy geçmişini tutan mantıksal bir kabuk olarak çalıştığını söylüyor. Bookstore App Service'e deploy edeceği için bizim environment'larımız boş: bookstore-dev, bookstore-staging ve bookstore-production.

Environment'ın getirdiği ilk şey deployment history. Pipelines altındaki Environments sayfasında her environment için hangi pipeline'ın hangi run'ının oraya deploy ettiği listeleniyor; bir deploy'un içinden Changes ve Work items sekmelerine geçince o deploy'la birlikte ilk kez o ortama giden commit'ler ve work item'lar görülüyor. Birden fazla pipeline aynı environment'a deploy ettiğinde ortamdaki değişikliğin hangi pipeline'dan geldiğini buradan buluyorsun. İkinci şey güvenlik: environment'ın kendi kullanıcı rolleri (Creator, Reader, User, Administrator) ve hangi pipeline'ların onu kullanabileceğini belirleyen pipeline permission'ları var. Üçüncüsü de bu yazının asıl konusu olan approval ve check'ler.

Bir ayrıntı: environment classic pipeline'larda yok, onların karşılığı deployment group. Stage odaklı bir görünüm, yani bir stage'i en son hangi run'ın çalıştırdığını tek ekranda görmek ise henüz gelmedi; Azure DevOps roadmap'inde "Stage-level traceability" adıyla planlanmış bir madde olarak duruyor ve dokuzuncu yazıda tarihine değineceğiz.

Deployment Job ve Lifecycle Hook'ları

Bir job'ı environment'a bağlamanın yolu onu job yerine deployment olarak yazmak. Deployment job'ı normal bir job'dan ayıran üç şey var: run'ı environment'ın geçmişine kaydediyor, adımları bir strategy altında lifecycle hook'larına bölüyor ve kaynak kodu otomatik olarak checkout etmiyor.

Strategy üç tane: runOnce, rolling ve canary. runOnce'ta preDeploy, deploy, routeTraffic ve postRouteTraffic hook'ları sırayla birer kez çalışıyor, ardından sonuca göre on: success ya da on: failure çalışıyor. rolling belgelere göre şu an yalnızca sanal makine resource'larıyla destekleniyor ve maxParallel ile her turda kaç hedefin güncelleneceğini belirliyor. canary, increments: [10, 20] gibi bir liste alıp deploy, routeTraffic ve postRouteTraffic'i her yüzde için tekrar çalıştırıyor; belgelerdeki örnek bunu Kubernetes için KubernetesManifest@1 ile yapıyor. Rolling ve canary'nin kendisi, yani ne zaman hangisinin seçileceği, Deployment Stratejileri serisinin Rolling Deployment ve Canary Release yazılarında anlatıldı; burada yalnızca YAML'daki karşılıkları önemli.

Her hook ayrı bir job olarak çalışıyor ve varsayılan olarak deployment job'ın pool'unu kullanıyor; pool: server verilirse agent gerektirmeyen bir server job'a dönüşüyor. Artifact indirme de hook'a bağlı: belgelere göre artifact'lar yalnızca deploy hook'unda otomatik indiriliyor ve indirilen şey, run'ın ve tanımlı bütün pipeline resource'larının bütün artifact'ları. Hangi artifact'ın ineceğini açıkça yazmak bu yüzden iyi bir alışkanlık.

Bookstore'un Deploy Stage'leri

Dördüncü yazıda Build stage'inin sonunda tek bir release artifact'ı üretmiştik: API'nin publish çıktısı, Angular'ın build çıktısı ve bir manifest.json. "Build once, deploy many" ilkesinin burada karşılığı şu: üç ortam da bu artifact'ı indiriyor, hiçbiri kodu yeniden derlemiyor. Ortak template repository'sine (Bookstore/pipeline-templates) bir stage template'i ekledik:

# Her environment için bir deployment stage'i. Build stage'inde bir kez üretilen release artifact'ını deploy eder.
# Approval ve check'ler burada değil: bookstore-<environment> environment'ının sahipleri onları web arayüzünde ekler.
parameters:
  - name: environment
    type: string
    values:
      - dev
      - staging
      - production
  - name: dependsOn
    type: string
  - name: mainOnly
    type: boolean
    default: false

stages:
  - stage: Deploy_${{ parameters.environment }}
    displayName: Deploy to ${{ parameters.environment }}
    dependsOn: ${{ parameters.dependsOn }}
    ${{ if eq(parameters.mainOnly, true) }}:
      condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
      lockBehavior: sequential
    ${{ else }}:
      condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
    pool:
      vmImage: ubuntu-24.04
    variables:
      - group: bookstore-${{ parameters.environment }}
    jobs:
      - deployment: Deploy
        displayName: Deploy release to ${{ parameters.environment }}
        environment: bookstore-${{ parameters.environment }}
        strategy:
          runOnce:
            deploy:
              steps:
                # Yalnızca release artifact'ı; api ve web zaten onun içinde.
                - download: current
                  artifact: release

                # Deployment job'lar repository'yi checkout etmez; script'ler orada duruyor.
                - checkout: self
                  fetchDepth: 1

                - script: bash scripts/verify-release.sh $(Pipeline.Workspace)/release
                  displayName: Verify release artifact
                  env:
                    EXPECTED_COMMIT: $(Build.SourceVersion)

Birkaç karar var. Variable group, ikinci yazıda gördüğümüz biçimle bağlanıyor: bookstore-${{ parameters.environment }} dosya işlenirken çözülüyor, çünkü variable group bir resource ve stage başlamadan yetkilendiriliyor. Environment adı da aynı şekilde template expression ile üretiliyor. Belgelerin sorun giderme bölümü bunun tersini açıkça uyarıyor: runtime parameter'lar environment oluştururken çalışmıyor, çünkü yalnızca run anında açılıyorlar. download: current ile yalnızca release'i istiyoruz; aksi hâlde api ve web de ikinci kez inerdi. checkout: self ise doğrulama script'i repository'de durduğu için gerekli.

Ortak iskelet (pipelines/bookstore-ci.yml) Build stage'inden sonra bu template'i üç kez, sabit bir sırayla çağırıyor. Uygulama pipeline'ı sırayı değiştiremiyor, yalnızca deploy parameter'ı ile deploy stage'lerini kapatabiliyor:

  # Build once, deploy many: her environment aynı release artifact'ını, sabit bir sırayla alır.
  - ${{ if eq(parameters.deploy, true) }}:
      - template: ../stages/deploy.yml
        parameters:
          environment: dev
          dependsOn: Build

      - template: ../stages/deploy.yml
        parameters:
          environment: staging
          dependsOn: Deploy_dev

      - template: ../stages/deploy.yml
        parameters:
          environment: production
          dependsOn: Deploy_staging
          mainOnly: true

Template'lerin yeni sürümü v1.3.0 ve uygulama pipeline'ı ref'i buna yükseltiyor. release/* branch'leri dev ve staging'e kadar gidiyor, main üçüne de; pull request run'ları hiçbirine.

Deploy'dan önceki ilk step, inen artifact'ın eksiksiz ve bu run'a ait olduğunu doğruluyor. Script, manifest'teki dosya sayılarını ve commit'i gerçek içerikle karşılaştırıyor. Yerelde API'yi dotnet publish ile, Angular'ı ng build ile derleyip dördüncü yazıdaki manifest script'iyle bir release klasörü oluşturduk ve üç durumu denedik:

$ EXPECTED_COMMIT=3f6c2a1d9e8b7c6a5f4e3d2c1b0a9f8e7d6c5b4a bash scripts/verify-release.sh out/release
Release 1.0.0-main.1412 verified: commit 3f6c2a1d9e8b7c6a5f4e3d2c1b0a9f8e7d6c5b4a, api 59 files, web 4 files
$ EXPECTED_COMMIT=9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b bash scripts/verify-release.sh out/release
##vso[task.logissue type=error]release was built from 3f6c2a1d9e8b7c6a5f4e3d2c1b0a9f8e7d6c5b4a, expected 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b
$ rm out/release/web/favicon.ico
$ bash scripts/verify-release.sh out/release
##vso[task.logissue type=error]web has 3 files, manifest says 4

download: current ile commit kontrolü her zaman geçer; dokuzuncu yazıda başka bir run'ın artifact'ını yeniden deploy ederken bu kontrolün değeri ortaya çıkacak.

Check Türleri ve Çalışma Sırası

Check'ler environment'larda, service connection'larda, repository'lerde, variable group'larda, secure file'larda ve agent pool'larda tanımlanabiliyor. Bir stage başlamadan önce, o stage'in kullandığı bütün resource'lardaki bütün check'lerin geçmesi gerekiyor. Belgeler beş kategoriyi ve sıralarını şöyle veriyor:

06-check-order-tr.png

Static check'ler en başta: branch control (run'a bağlı bütün kaynakların izin verilen branch'lerden ve istenirse korumalı branch'lerden geldiğini doğrular), required template (pipeline belirli bir template'ten extends etmiyorsa run'ı düşürür) ve evaluate artifact (şimdilik yalnızca container image'ları için bir policy değerlendirmesi). Ardından pre-check approval'lar, sonra dynamic check'ler geliyor: approval, business hours (stage'i belirli bir zaman aralığına bekletir), Invoke Azure Function, Invoke REST (HTTP üzerinden kaynak odaklı API tarzı) API ve Query Azure Monitor alerts. Onları post-check approval'lar ve en sonda exclusive lock izliyor. Her kategorinin içinde check'ler oluşturuldukları sırayla çalışıyor ve sıralama environment'ın Approvals and checks sekmesinde görülebiliyor.

Bu sıranın pratik sonucu şu: Selin, branch control'den geçemeyecek bir run için onay vermek zorunda kalmıyor. Bir check kesin olarak reddederse (örneğin approval reddedilirse) stage çalışmıyor. Approval belirlenen süre içinde verilmezse stage atlanmış (skipped) olarak işaretleniyor; belgelere göre zaman aşımına uğrayan approval ve check'lerden sonra stage yeniden denenebiliyor.

Production'a Yalnızca main'den ve Onayla

bookstore-production environment'ının Approvals and checks sekmesine dört check ekledik. Branch control, izin verilen branch olarak refs/heads/main alıyor; belgeler branch adının tam biçimde yazılmasını istiyor. "Korumalı branch şartı" da açık: en az bir branch policy'si olan branch korumalı sayılıyor. Required template, Bookstore/pipeline-templates repository'sini, refs/tags/v1.3.0 ref'ini ve pipelines/bookstore-ci.yml yolunu gösteriyor; üçüncü yazıda kurduğumuz iskelet burada zorunlu hâle geliyor. Approval, onaylayıcı olarak "Bookstore Release Approvers" grubunu alıyor; gruptan tek bir kişinin onayı yetiyor ve run'ı başlatan kişinin kendi run'ını onaylamasını engelleyen seçeneği işaretliyoruz. Son olarak exclusive lock.

Exclusive lock, template'teki lockBehavior: sequential ile birlikte çalışıyor. Lock varken aynı pipeline'ın birden fazla run'ı production'a aynı anda giremiyor. sequential run'ları sırayla geçiriyor; varsayılan olan runLatest ise kilidi yalnızca en yeni run'a veriyor, bekleyen eskileri atlıyor. Production için sıralı davranışı seçtik, çünkü her main run'ı bir migration taşıyabilir ve hiçbirinin atlanmasını istemiyoruz.

Belgeler, onaylanacak kişilerin listesinin check'ler çalışmaya başladığı anda sabitlendiğini de söylüyor. Sonradan gruba eklenen biri o run için onay veremiyor. Onay verildiği an ile deploy'un başlaması gereken an farklıysa, örneğin akşam trafiğin düştüğü saatte deploy etmek istiyorsan, "Defer approval" ile onayın geçerli olacağı zamanı seçebiliyorsun.

Classic Release'teki Karşılıkları

Classic release pipeline'ından gelen ekipler için eşleme şöyle:

Classic releaseYAMLFark
Stage (environment)stage + deployment job + environment resourceGeçmiş environment'ta tutulur, birden fazla pipeline'ı kapsar
Pre-deployment approvalEnvironment'ta approval check'iPipeline'da değil resource'ta; YAML yazarı değiştiremez
Post-deployment approvalSonraki stage'in kullandığı environment'ta approvalDoğrudan bir karşılığı yok; bir sonraki ortamın girişine taşınır
Gate (REST API, Azure Function, Azure Monitor)Invoke REST API, Invoke Azure Function, Query Azure Monitor alerts check'leriQuery work items ve Azure Policy gate'lerinin check karşılığı yok
Zamanlanmış release / deploy penceresiBusiness hours check'iStage pencere açılınca başlar
Deployment queue ayarlarıExclusive lock + lockBehaviorrunLatest ya da sequential
Deployment groupSanal makine resource'u olan environmentEnvironment classic'te kullanılamaz

Bir zaman aşımı farkı da var: classic'te süresinde verilmeyen onay deploy'u reddediyor, YAML'da ise stage atlanıyor. Classic gate'ler belgelere göre bütün gate'ler aynı örnekleme aralığında başarılı olana kadar tekrar değerlendiriliyor; YAML'daki Invoke REST API ve Azure Function check'lerinde de "Time between evaluations" ayarı aynı işi görüyor.

Üretimde Dikkat

Required template check'i bir ref'e bağlı. Check refs/tags/v1.3.0'ı gösterirken template'leri v1.4.0'a yükselten bir pipeline production'a giremiyor. Template sürümünü yükseltmek bu yüzden iki adımlı bir iş: önce check'e yeni ref'i eklemek (bir resource'ta birden fazla required template tanımlanabiliyor), sonra pipeline'ları yükseltmek, en son eski ref'i kaldırmak.

Approval bekleyen bir run açık kalıyor ve sequential lock varken arkasındaki run'lar da onu bekliyor. Onaylayıcı grubunun tatil dönemlerinde boş kalmaması ve approval süresinin ekibin gerçek tepki süresine göre ayarlanması gerekiyor.

Check'leri yalnızca production'a koymak yeterli değil. Production'a deploy eden stage bir Azure service connection da kullanacak; yedinci yazıda aynı required template ve branch control check'lerini o connection'a da ekleyeceğiz. Böylece birisi environment'ı atlayıp aynı connection'la doğrudan deploy eden ayrı bir pipeline yazsa bile kontrol devrede kalıyor.

Sık Düşülen Tuzaklar

Deployment job'ın repository'yi checkout ettiğini varsaymak. Etmiyor. Script'in "No such file or directory" ile kırılmasının en sık nedeni bu; checkout: self eklemek gerekiyor.

Var olmayan bir environment'a CI trigger'ıyla gitmek. Pipeline'ı web editöründe kaydeden bir kullanıcı varsa Azure Pipelines environment'ı otomatik oluşturuyor; ama YAML başka bir editörde değişip run bir trigger'la başlarsa kimin adına oluşturulacağı bilinmediği için job "could not be found" hatasıyla düşüyor. Environment'ları pipeline'dan önce, elle oluştur.

Check'i YAML'da aramak. approval: diye bir anahtar yok; check'ler resource'un sahibine ait ve web arayüzünden ya da Approvals and Checks REST API'si ile yönetiliyor. Pipeline'ı kodla incelerken check'leri ayrıca gözden geçirmek gerekiyor.

Approval zaman aşımını hesaba katmamak. Süresinde onaylanmayan stage atlanıyor ve ona bağlı stage'ler de çalışmıyor; run başarısız görünmeyebilir ama production'a hiçbir şey gitmemiş olur. Stage'i yeniden denemek mümkün, yeter ki bunu fark eden biri olsun.

Küçük Bir Deneme

★☆☆ verify-release.sh'e, manifest'teki version değerinin boş olmadığını kontrol eden bir satır ekle ve boş bir manifest'le dene.

★★☆ Deploy template'ine on: failure hook'u ekle: deploy başarısız olursa run'a bir uyarı yazsın ve hangi environment'ta kırıldığını söylesin. Bu hook'un hangi agent'ta çalışacağını belgelerden kontrol et.

★★★ bookstore-staging için bir business hours check'i ve bookstore-production için deferred approval kullanan bir akış tasarla: staging mesai saatinde, production ertesi sabah 07:00'de deploy olsun. Hangi check'in hangi resource'ta duracağını bir tablo olarak yaz.

Stage'ler hazır ama henüz hiçbir şeyi Azure'a göndermiyorlar. Bir sonraki yazıda deploy step'lerini dolduracağız: workload identity federation ile şifresiz bir Azure service connection, App Service staging slot'una deploy, smoke test ve swap, Key Vault'tan gelen bağlantı bilgisiyle Entity Framework Core (EF Core) migration bundle'ı.