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

Azure Pipelines Agent'ları ve Güvenlik: Microsoft-Hosted, Self-Hosted, Managed DevOps Pools ve Protected Resource'lar

Mert Özen 15 Kas 2026 10 dk 7 görüntülenme
Azure Pipelines Agent'ları ve Güvenlik: Microsoft-Hosted, Self-Hosted, Managed DevOps Pools ve Protected Resource'lar

Microsoft-hosted agent her job için yeni bir makine verir ama ücretsiz katmanda tek parallel job ve 60 dakika sınırı var; daha fazlası için self-hosted agent ya da Managed DevOps Pools gerekir. Bookstore için tek job'lık bir container agent'ı hazırlıyor, image takvimini, job authorization scope'u, protected resource'ları, settableVariables ile değişken kısıtlamayı ve Node.js runner'larının kaldırılma tarihlerini görüyoruz.

Sürüm notu: Bu yazıdaki self-hosted agent image'ı Docker 29 ile yerelde derlenip çalıştırılmıştır; pipeline ve template dosyaları yq 4.54 ile ayrıştırılarak denetlenmiştir. Agent seçeneklerinin, Managed DevOps Pools'un, parallel job limitlerinin ve güvenlik ayarlarının 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çı) CI pipeline'ında Build stage'i dört job'la başlıyor: Version bittikten sonra Api ve Web paralel, Audit ise hiçbir şeye bağlı değil. Ekip yeni bir Azure DevOps organization'ı açmış, hiçbir şey satın almamış ve Microsoft-hosted ücretsiz katmanı etkinleştirmiş. Bu üç job aynı anda koşar mı, ve bir job 70 dakika sürerse ne olur?

Koşmaz. Belgelere göre ücretsiz katman private project'ler için tek bir Microsoft-hosted parallel job veriyor: her seferde en fazla 60 dakika, ayda toplam 1.800 dakika (30 saat). Parallel job organization seviyesinde, bütün project'ler ve pipeline'lar arasında paylaşılıyor. YAML (girintiyle yapı kuran veri formatı) dosyasında dependsOn: [] yazmak job'ların paralel koşabileceğini söylüyor, koşacağını değil; tek bir parallel job varken Api, Web ve Audit sıraya giriyor. 70 dakikalık bir job ücretsiz katmanda 60. dakikada kesiliyor; ücretli bir parallel job'da sınır job başına 360 dakika. Ücretsiz katmanı almak için organization'ı bir Azure subscription'a bağlayıp faturalandırmayı kurmak gerekiyor; ilk satın alınan parallel job sayıyı ikiye çıkarmıyor, yalnızca ilk job'ın zaman sınırlarını kaldırıyor.

Microsoft-Hosted Agent'lar: Image'lar ve Sınırlar

Microsoft-hosted agent her job için yeni bir sanal makine demek: job bitince makine atılıyor, bir sonraki job öncekinin dosya sisteminde bıraktığı hiçbir şeyi görmüyor. Linux ve Windows makineleri 2 çekirdek, 7 GB RAM ve 14 GB SSD (katı hal diski) alanı olan Azure sanal makineleri; pipeline'ın kullanabileceği boş alan en az 10 GB. Bu donanım büyütülemiyor; daha güçlü makine gerekiyorsa yol self-hosted agent ya da Managed DevOps Pools.

Image etiketleri belgelerde 9 Kasım 2026 itibarıyla şöyle: ubuntu-latest ubuntu-24.04'ü gösteriyor; ubuntu-26.04 29 Eylül 2026'da GA (genel kullanıma açık) oldu; ubuntu-22.04 deprecated ve 17 Nisan 2027'de kaldırılacak, öncesinde brownout'lar (belirli saatlerde bilerek hata verdirme) planlı. Windows tarafında windows-latest şu an windows-2025'i gösteriyor; Visual Studio 2026'lı windows-2025-vs2026 image'ı 29 Eylül 2026'da GA oldu ve belgeye göre önümüzdeki haftalarda windows-latest ona geçecek, windows-2025 da Visual Studio 2026'yı kullanmaya başlayacak. windows-2019 31 Aralık 2025'te emekliye ayrıldı. Bu seride vmImage: ubuntu-24.04 yazmamızın nedeni bu hareketlilik: -latest etiketi bir gün habersiz başka bir işletim sistemine geçebiliyor.

Ağ tarafında iki not var. Hosted agent'lara public IP atanmıyor, yani dışarıdan erişilemiyorlar. Ama şirket ağındaki bir kaynağa erişmeleri gerektiğinde, organization'ın coğrafyasındaki bütün Azure bölgelerinin IP aralıklarını açmak gerekiyor; belgeler bu aralıkların geniş olduğunu ve başka müşterilerin makinelerini de içerdiğini hatırlatıyor.

Self-Hosted Agent: Container İçinde, Tek Job'lık

Self-hosted agent kendi makinende ya da container'ında çalışan agent yazılımı. Kayıt sırasında kimlik doğrulama için belgeler Azure DevOps Services'ta üç yol sayıyor: personal access token (PAT, kişisel erişim token'ı), device code flow ve service principal. Kayıt bir kez yapılıyor; agent günlük çalışmasında kaydı yapan kişinin kimliğini kullanmıyor.

Bookstore için Ubuntu 24.04 tabanlı bir agent image'ı hazırladık. Belgelerdeki container agent örneğini temel alıp sadeleştirdik:

# Bookstore build'leri için self-hosted Azure Pipelines agent'ı. Her container tam olarak bir job çalıştırır (start.sh'e bak).
FROM ubuntu:24.04

ENV TARGETARCH="linux-x64"

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git jq libicu74 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /azp
COPY ./start.sh ./
RUN chmod +x ./start.sh \
    && useradd -m -d /home/agent agent \
    && chown -R agent:agent /azp /home/agent

# Agent sudo yetkisi olmayan, root olmayan bir kullanıcıyla çalışır.
USER agent
ENTRYPOINT ["./start.sh"]
#!/usr/bin/env bash
# Geçici agent: kendini kaydeder, tam olarak bir job çalıştırır (--once) ve çıkarken kaydını siler.
# Bir sonraki job için orchestrator yeni bir container başlatır; bir job'dan diğerine hiçbir şey kalmaz.
set -euo pipefail

: "${AZP_URL:?AZP_URL is required, for example https://dev.azure.com/bookstore-org}"
: "${AZP_TOKEN_FILE:?AZP_TOKEN_FILE is required: a mounted file that holds the registration token}"
pool="${AZP_POOL:-bookstore-linux}"
agent_name="${AZP_AGENT_NAME:-bookstore-$(hostname)}"

# Export edilmeyen bir shell değişkeni: run.sh'in başlattığı job'lar token'ı görmez.
token="$(cat "$AZP_TOKEN_FILE")"

package_url=$(curl -fsS -u "agent:$token" \
  "$AZP_URL/_apis/distributedtask/packages/agent?platform=$TARGETARCH&top=1" | jq -r '.value[0].downloadUrl')
curl -fsSL "$package_url" | tar -xz

cleanup() {
  VSTS_AGENT_INPUT_TOKEN="$token" ./config.sh remove --unattended --auth pat || true
}
trap cleanup EXIT

# config.sh --token değerini VSTS_AGENT_INPUT_TOKEN'dan okur; token hiçbir komut satırında görünmez.
VSTS_AGENT_INPUT_TOKEN="$token" ./config.sh --unattended --url "$AZP_URL" --auth pat \
  --pool "$pool" --agent "$agent_name" --replace --work _work

./run.sh --once

Üç karar var. run.sh --once agent'ı tek bir job'dan sonra kapatıyor; container yeniden başlatılınca temiz bir agent geliyor. Belgelere göre self-hosted agent'ta varsayılan olarak yalnızca artifact staging ve test sonuçları klasörleri run'lar arasında temizleniyor; kalıcı bir agent'ta bir job'ın bıraktığı dosya sonrakine kalıyor. Agent belgelerin önerdiği gibi sudo yetkisi olmayan ayrı bir kullanıcıyla çalışıyor. Token da bir environment variable yerine bağlanan bir dosyadan okunuyor; config.sh her komut satırı parametresini VSTS_AGENT_INPUT_ önekli bir environment variable olarak da kabul ettiği için token komut satırına yazılmıyor. Image'ı yerelde derleyip ayarsız çalıştırdık:

$ docker run --rm bookstore-agent:dev
./start.sh: line 6: AZP_URL: AZP_URL is required, for example https://dev.azure.com/bookstore-org
$ docker run --rm -e AZP_URL=https://dev.azure.com/bookstore-org bookstore-agent:dev
./start.sh: line 7: AZP_TOKEN_FILE: AZP_TOKEN_FILE is required: a mounted file that holds the registration token
$ docker run --rm --entrypoint id bookstore-agent:dev
uid=1001(agent) gid=1001(agent) groups=1001(agent)

Uygulama ve test için gereken araçlar (.NET SDK, Node.js) image'da yok; pipeline'daki UseDotNet@2 ve UseNode@1 onları her job'da kuruyor. Image'a gömmek job'ları hızlandırır ama image'ı güncel tutma işini sana bırakır.

Managed DevOps Pools

Kendi agent'larını otomatik ölçeklemek isteyenler için eski yol Virtual Machine Scale Set agent pool'larıydı. Managed DevOps Pools onların devamı: Azure DevOps blog'unda 18 Kasım 2024'te GA olarak duyuruldu. Roadmap açıkça yazıyor: yeni senaryolar için özellikler Scale Set pool'larına değil, Managed DevOps Pools'a ekleniyor ve Scale Set pool'larının oraya taşınması öneriliyor.

Belgelerdeki özellikler, hosted agent'ların sınırlarına doğrudan cevap veriyor: istediğin VM boyutu, Microsoft-hosted image'larıyla aynı yazılımı içeren image'lar ya da kendi image'ın, şirket ağına erişebilen ya da yalnızca belirli adreslere çıkabilen ağ ayarları, job başına iki güne kadar süre ve agent durumunu yedi güne kadar koruyarak cache'ten yararlanma. Makineler senin değil, Microsoft'un yönettiği bir subscription'da çalışıyor; sen bir Azure kaynağı olarak pool'u yönetiyorsun. Eylül 2026 sürümüyle bir pool'da beşe kadar VM boyutu tanımlanabiliyor (Instance Mix); spot sanal makine desteği ise roadmap'te tarihsiz duruyor. Bookstore'un production veritabanı private endpoint arkasına taşınırsa migration bundle'ını çalıştıran stage için doğal seçim bu: pool: name: bookstore-mdp ile yalnızca o stage'i bu pool'a yönlendirmek.

Job Authorization Scope ve Build Kimliği

Beşinci yazıda feed izinlerini anlatırken iki build kimliğinden söz etmiştik: organization seviyesindeki Project Collection Build Service (bookstore-org) ve project seviyesindeki Bookstore Build Service (bookstore-org). Her job için Azure Pipelines bir job access token üretiyor; token'ın yetkisi job authorization scope'a ve bu kimliklere verilen izinlere göre belirleniyor. Scope collection ise job organization'daki bütün project'lerin repository'lerine erişebiliyor; project ise yalnızca kendi project'ininkilere.

Belgelerin önerisi net: Organization settings altında "Limit job authorization scope to current project for non-release pipelines" ayarını açmak. Ayar organization seviyesinde açılınca project'ler onu kapatamıyor; belgeler bunun nedenini de söylüyor: kısıt yoksa tek bir pipeline'ı ele geçiren biri organization'daki bütün repository'lere erişebiliyor. Release pipeline'ları için ayrı bir ayar var. Buna ek olarak "Protect access to repositories in YAML pipelines" açıkken job yalnızca checkout ya da uses ile açıkça başvurduğu repository'lere erişiyor; bu ayar Mayıs 2020'den sonra oluşturulan organization ve project'lerde varsayılan olarak açık. Bookstore'un CI'ı pipeline-templates repository'sini resources ile tanımladığı için bu ayardan etkilenmiyor.

Protected Resource'lar ve Pipeline Permission'ları

Agent pool'lar, environment'lar, service connection'lar, repository'ler, secret içeren variable group'lar ve secure file'lar protected resource. Her birinin iki katmanı var: pipeline permission'ları (hangi pipeline'lar kullanabilir) ve check'ler (kullanmadan önce ne sağlanmalı). Bir pipeline bir resource'u ilk kez kullandığında yetki verilene kadar run duruyor. "Open access" seçeneği project'teki bütün pipeline'lara izin veriyor; içinde secret ya da production erişimi olmayan resource'lar için makul. Managed DevOps Pools da varsayılan olarak her pipeline'ın ayrıca yetkilendirilmesini istiyor; açık erişim Nisan 2025'te seçenek olarak eklendi.

Belgelerin bir uyarısı da classic pipeline'larla ilgili: YAML ve classic pipeline'lar aynı service connection'ları paylaşabildiği için, yalnızca YAML kullanan bir organization'ın classic build ve release pipeline oluşturmayı kapatması öneriliyor. İkisi birden kapatılınca task group ve deployment group da oluşturulamıyor. Bunu dokuzuncu yazıda geçiş planının parçası olarak ele alacağız.

Değişkenleri Kısıtlamak: settableVariables ve Queue-Time Ayarı

İkinci yazıda gördüğümüz gibi bir script ##vso[task.setvariable] ile yeni değişken tanımlayabiliyor ya da var olanın değerini değiştirebiliyor. Belgelerdeki örnek riski gösteriyor: bir sonraki step $(MyConfig)'u bir komutun içine koyuyorsa, değeri değiştiren önceki step o komuta istediğini ekleyebilir. Template'lerin v1.5.0 sürümünde bunu step seviyesinde kısıtladık:

          - script: bash ${{ parameters.testDbScript }}
            displayName: Start SQL Server for integration tests
            # Script yalnızca bu değişkeni ayarlayabilir; başka bir şey denerse uyarıyla yok sayılır.
            target:
              settableVariables:
                - TestDbConnection
          - script: |
              dotnet list ${{ parameters.solution }} package --vulnerable --include-transitive | tee audit.txt
              if grep -q "has the following vulnerable packages" audit.txt; then
                echo "##vso[task.logissue type=error]Vulnerable packages found."
                exit 1
              fi
            displayName: Fail on vulnerable packages
            target:
              settableVariables: none

Belgelere göre listede olmayan bir değişkeni ayarlamaya çalışan step'te değişken ayarlanmıyor ve run sayfasında bir uyarı görünüyor; none ise hiçbir değişkenin ayarlanmasına izin vermiyor. Deploy template'indeki doğrulama, migration ve smoke test step'lerine de none ekledik; hiçbirinin değişken üretmesi gerekmiyor.

Run başlatılırken eklenen değişkenler ayrı bir kapı. "Limit variables that can be set at queue time" ayarı açıkken kullanıcı yalnızca "Let users override this value when running this pipeline" olarak işaretlenmiş değişkenleri değiştirebiliyor, yeni değişken ekleyemiyor; REST (HTTP üzerinden kaynak odaklı API tarzı) API ile denenirse istek hata alıyor. YAML'da tanımlanan bir değişkeni readonly: true ile salt okunur yapmak ve "Enable shell tasks arguments validation" ayarını açmak da aynı listede. Kullanıcıdan seçim almak gerektiğinde değişken yerine values listesi olan bir runtime parameter kullanmak en güvenli yol: belgelerin vurguladığı gibi çalışan bir pipeline parameter'ı değiştiremiyor.

Üretimde Dikkat

Task'ların Node.js sürümlerini takip et. Belgelere göre resmî task'lar şu an Node.js 20 handler'ı kullanıyor; Node.js 20'nin Azure Pipelines'taki desteği Nisan 2026'da bitti, agent'tan kaldırılması Nisan 2027'de. Node.js 6, 10 ve 16 ise 24 Kasım 2026'da agent'tan kaldırılıyor ve agent Node.js 24'ü zaten içeriyor. Marketplace'ten eski bir task kullanıyorsan, Organization settings altındaki "Restrict out of support Node.js versions in pipeline tasks" ayarını açıp log'daki uyarılara bakmak etkiyi önceden gösteriyor. Roadmap'te yerleşik task'ların Node.js 24'e geçişi tarihsiz bir madde olarak duruyor.

PAT'leri azalt. Roadmap'in credential theft girişimi PAT gibi çalınabilecek secret'lara olan ihtiyacı azaltmayı ve Entra ID token'larına yönelmeyi hedefliyor. Agent kaydı için service principal kullanmak, yedinci yazıdaki federation connection'ları ve build kimliğinin kendi token'ı bu yöndeki adımlar.

Secret masking'e güvenme. Belgeler log'daki secret'ları silmeye çalıştıklarını ama bunun kusursuz olmadığını söylüyor ve secret'ı JSON gibi yapılandırılmış bir değerin içine koymamayı öneriyor; tek tek secret'lar daha doğru maskeleniyor.

Sık Düşülen Tuzaklar

Kalıcı bir self-hosted agent'ta önceki job'ın dosyalarını unutmak. Varsayılan olarak yalnızca iki klasör temizleniyor. Kalıcı agent kullanıyorsan job'a workspace: clean: all ekle ya da Bookstore'daki gibi her job için yeni agent başlat.

Tek bir self-hosted pool'u bütün project'lere açmak. Belgeler aynı agent'ı birden fazla project'in sırayla kullanmasının project'ler arası sızma riski taşıdığını söylüyor ve project başına ayrı pool öneriyor. Fork'lardan gelen pull request'leri ise hiç self-hosted agent'ta çalıştırma.

Fork build'lerine secret açmak. Varsayılan olarak fork'tan gelen pull request doğrulamalarına secret ve protected resource verilmiyor; "Make secrets available to builds of forks" seçeneğini açmak bu korumayı kaldırıyor.

Agent'ı yetkili bir kullanıcıyla çalıştırmak. Entra ID'deki bir kimlikle çalışan agent, job token'ına ihtiyaç duymadan Azure DevOps API'lerine erişebiliyor; ele geçirilmiş bir pipeline bütün organization'a uzanabiliyor.

Küçük Bir Deneme

★☆☆ set-version.sh'i çalıştıran step'e settableVariables ekle ve yalnızca appVersion'a izin ver. Output variable'ların bu listeyle nasıl davrandığını belgelerden kontrol et.

★★☆ Agent image'ına .NET 10 SDK'yı ekle, image boyutunu ve bir job'ın süresini UseDotNet@2'li hâliyle karşılaştır.

★★★ Bookstore için bir agent stratejisi yaz: hangi stage'ler Microsoft-hosted'da, hangileri Managed DevOps Pools'ta koşmalı? Ağ, maliyet ve güvenlik açısından gerekçelendir.

Pipeline'lar YAML'da, deploy'lar şifresiz, agent'lar ve ayarlar sıkı. Serinin son yazısında geriye bakacağız: classic build ve release pipeline'larından YAML'a nasıl geçilir ve 2023'ten bu yana Azure Pipelines'ta neler değişti.