Azure Pipelines Task Extension’ı esbuild ile Küçültmek
Şunu bir düşünün: Her pipeline job’unda, her agent’ta, günde binlerce kez indirilen bir task extension’ınız var. İçinde de on binlerce minicik dosya ve koca bir node_modules klasörü dürüyor. Ne oluyor? Agent, task’ı çalıştırmadan önce saniyeler içinde bile olsa bir “hazırlık” süresi yiyor. Çarpın bunu binle, on binle… Fatura şişiyor.
Microsoft DevOps ekibinden David Paquette geçenlerde güzel bir yazı paylaştı — kendi iç Azure Pipelines task extension’larını esbuild ile bundle edip, task başına yaklaşık ~4.5 saniyeden ~0.25 saniyeye indirmişler. Yanı 17 kat hızlanma. Üstelik bunu 20 satır civarı build tooling ile yapmışlar. İşin garibi şu: Bir yanıyla aşırı basit, öbür yanıyla da insan “bunu neden daha önce kimse kurcalamadı?” diye düşünüyor.
Durun, bir saniye.
Ben de sahada bu tarz görevleri yazan ve dağıtan ekiplerle sık sık çalışıyorum, o yüzden konuya kendi cephemden bakayım istedim. Türkiye’deki kurumsal DevOps ekipleri için de konu baya önemli; özellikle self-hosted agent maliyetlerini kısmak isteyenler varsa, burada gerçekten elle tutulur bir kazanç çıkabiliyor. Evet.
Peki, peki neden?
Çünkü mesele sadece “kodun çalışması” değil. Asıl yük çoğu zaman paketleme tarafında çıkıyor; küçük küçük dosyalar, gereksiz bağımlılıklar, her koşuda yeniden açılan klasörler derken, sistem aslında iş yapmaktan çok hazırlık yapıyor gibi davranıyor. Hani bazen bir şeyin kendisi değil de etrafındaki kalabalık yoruyor ya, tam öyle.
Burada esbuild’in olayı devreye giriyor. Dosyaları tek bir bundle’a topluyorsunuz, dependency ağacını sadeleştiriyorsunuz, sonra agent’ın önüne daha temiz bir çıktı koyuyorsunuz; böylece runtime’da gezilecek yer azalıyor. Startup süresi gözle görülür biçimde düşüyor. Şey yanı, sihir falan değil ama pratikte baya iş görüyor.
Bunu biraz açayım.
Tabiî şunu da söyleyeyim: Her senaryoda aynı sonucu beklemek biraz fazla iyimser olur. Eğer task zaten hafifse veya bağımlılık sayısı azsa, kazanım o kadar dramatik olmayabilir; ama büyük extension’larda fark açık konuşayım can yakıyor. Daha açık söyleyeyim, sız ne dersiniz?
Neyse uzatmayalım.
Araya gireyim: Bu tip iyileştirmelarda en sevdiğim taraf şu: Kod değiştirmeden önce mimariyi biraz toparlayınca ciddi fark çıkabiliyor. Yanı “daha hızlı donanım alalım” demeden önce, dağıttığınız paketin içinde ne taşıdığını bir kontrol etmek lazım. Yukarıda bahsettiğim olay var ya, işte tam orada maliyet sessizce büyüyor.
Neden Task Boyutu Düşündüğünüzden Daha Önemli?
Klasik bir hikâye var burada. Node.js tabanlı bir Azure DevOps task’ı yazıyorsunuz, TypeScript ile başlıyorsunuz, azure-pipelines-task-lib ve birkaç yardımcı npm paketi ekliyorsunuz, sonra transitive dependency’ler bir anda üstünüze yığılıyor… Bir bakmışsınız task klasöründe binlerce dosya var, build alınca da bu klasör olduğu gibi paketleniyor, ardından gidip vsix’in içine giriyor.
Bunu biraz açayım.
Eh, Agent tarafında ne oluyor peki? Job “Initialize” aşamasında şunları yapıyor:
- Task’ın içerik zıp’ını Azure DevOps’tan indiriyor
- Bunu diske extract ediyor
- Node runtime’ı ayağa kaldırıyor
- Ondan sonra sizin kodunuz çalışmaya başlıyor
İlginç olan şu ki, Ephemeral agent kullanıyorsanız — ki Microsoft-hosted agent’lar ya da her job için sıfırdan başlayan self-hosted konfigürasyonlar tam olarak böyle çalışıyor — cache diye bir şey yok. Her seferinde her şey baştan indiriliyor. Hani “task’ım küçük, bir MB’ı geçmez” diyebilirsiniz. Ama işin aslı boyutta değil, dosya sayısında. Hani ne farkı var diyorsunuz, değil mi? Kulağa ufak geliyor ama pratikte fark ediyor.
Zıp formatının doğası gereği, 10 MB’lık tek bir dosyayı açmak, toplam 10 MB tutan 10.000 küçük dosyayı açmaktan kat kat hızlı. I/O overhead’i, dosya sistemi metadata operasyonları, kısacası her dosya için ayrı bir “muhasebe” işi yapılır. Bundle etmek işte tam da bunu ortadan kaldırıyor.
Peki Rakamlar Ne Diyor?
Yanı, Microsoft ekibinin ölçümleri şöyle çıkmış:
Peki neden?
| Metrik | Önce | Sonra |
|---|---|---|
| Task başına download + extract | ~4.5 sn | ~0.25 sn |
| 10 saniyeden uzun süren indirmeler | Yüksek | %98 azalma |
| Task klasöründeki dosya sayısı | Binlerce | 3 (script.js, task.json, icon.png) |
| Paket boyutu | On MB’lar | Tek digit MB |
%98. Bu rakam insanı durduruyor biraz. Yanı pratikte “download uzun sürdü” derdi neredeyse tamamen bitmiş gibi görünüyor.
Evet.
Açık konuşayım, burada mesele sadece hız da değil; daha az dosya demek daha az sürpriz demek, daha az sürpriz de pipeline içinde daha sakın sabahlar demek (özellikle onlarca task kullanan ekiplerde). Bir bakıma, sız ne dersiniz?
Bundling: Sadece Tarayıcı İşi Değil
Şöyle söyleyeyim, Bundler ve tree-shaking denince çoğumuzun aklına direkt front-end geliyor, değil mi? Webpack, Vite, esbuild… kafa oraya gidiyor hemen. “Kullanıcıya daha az JS gönderelim, sayfa hızlı açılsın” mantığı var ya, işte o. Server-side ya da CLI tarafında işe genelde “aynı makinede çalışıyor, bundle etmeye ne gerek var” diye geçiliyor.
Yanlış düşünce bu. Bak şimdi, pipeline task’ı aslında dağıtılan bir artifact. Her build agent’ı önü her seferinde baştan indiriyor, hem de defalarca; günde binlerce kez olunca olayın rengi değişiyor (ilk duyduğumda inanamadım). Bu tam da bundling’in çözdüğü problem. Front-end dünyasında bu maliyeti tarayıcı ödüyor, burada işe build agent ödüyor. Maliyet aynı, sadece cebi değişiyor.
Aynı mantık npm-published CLI’lar, serverless function paketleri, hatta container image layer’ları için de geçerli. Eğer artifact’ınız koca bir — en azından ben öyle düşünüyorum — node_modules ağacını sırtında taşıyorsa — hiç çağırmadığınız kodu tree-shaking ile ayıklamak, kalan parçayı da tek dosyada toparlamak — nereye deploy ederseniz edin fark yaratıyor. Küçük gibi dürüyor ama değil.
Türkiye’deki Kurumsal Ekipler İçin Not
Şimdi işin Türkiye tarafına gelelim. Birçok kurumsal müşteride self-hosted agent kullanılıyor; sebep de malum: güvenlik, network izolasyonu, on-prem kaynaklara erişim. Bu agent’lar çoğu zaman Azure VM’lerinde dönüyor ya da direkt on-prem ortamda koşturuluyor. Kulağa sıradan geliyor ama pratikte hikâye biraz daha sert.
Şimdi gelelim işin can alıcı noktasına.
İşin garibi, Self-hosted agent’ta üç senaryo var:
- Persistent agent: Aynı VM sürekli ayakta kalıyor, task’lar cache’lenebiliyor. Bundling’in etkisi ilk çalıştırmada net hissediliyor, sonra açıkçası çok dramatik olmuyor.
- Ephemeral agent (Scale Set Agents): Her job için yeni VM ayağa kalkıyor. Task her seferinde yeniden indiriliyor. İşte bundling burada baya işe yarıyor.
- Container-based agent: Her job fresh container içinde çalışıyor. Yine her seferinde download var, sürpriz yok.
Şöyle ki, Kurumsal ekiplerde son yıllarda genel yön Scale Set Agents veya container tabanlı ephemeral kurulumlara kaydı diyebilirim. Sebep basit: güvenlik. Bir job’un bir sonrakini kirletmesi istenmiyor, haklı olarak da istenmiyor. Az önce bunu doğru tercih diye söyledim ama küçük bir bedeli var: cold start maliyeti. Task’larınızı bundle edince o bedelin bir kısmını sessizce siliyorsunuz.
Çözüm: Tek Dosyada Toplamak
İşin güzel tarafı şu: çözüm aslında göz korkutmuyor. esbuild’e, “bak şu entry point’ten başla, bağımlılıkları tek tek izle, sonra hepsini bir dosyada topla” demeniz yetiyor (ki bu çoğu kişinin gözünden kaçıyor) — valla güzel iş çıkarmışlar —. Sonunda task klasörü üç parçaya düşüyor, biraz da şaşırtıcı şekilde sadeleşiyor:
Evet, doğru duydunuz.
script.js— bundle edilmiş, büyük çoğunluk bağımlılıklarıyla birlikte tek dosyatask.json— Azure DevOps’un task’ı tanıyabilmesi için manifesticon.png— UI’daki ikon
Kendi deneyimimden konuşuyorum, Bak şimdi, örnek bir build script’i şöyle olabilir:
// build-task.js
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');
const taskDir = 'tasks/myTask';
const outDir = 'dist/tasks/myTask';
// 1) Bundle et
esbuild.buildSync({
entryPoints: [path.join(taskDir, 'index.ts')],
bundle: true,
platform: 'node',
target: 'node20',
outfile: path.join(outDir, 'script.js'),
minify: true,
external: [], // Node built-in'ler zaten hariç
format: 'cjs',
});
// 2) task.json ve icon.png'yi kopyala
fs.copyFileSync(
path.join(taskDir, 'task.json'),
path.join(outDir, 'task.json')
);
fs.copyFileSync(
path.join(taskDir, 'icon.png'),
path.join(outDir, 'icon.png')
);
// 3) task.json'daki execution.Node20.target'ı script.js'e çevir
const manifest = JSON.parse(
fs.readFileSync(path.join(outDir, 'task.json'), 'utf8')
);
manifest.execution.Node20.target = 'script.js';
fs.writeFileSync(
path.join(outDir, 'task.json'),
JSON.stringify(manifest, null, 2)
);
console.log('Task bundled:', outDir);
Gördüğünüz gibi işin özü bu kadar. Yirmi beş satır falan ediyor; küçük görünüyor ama etkisi baya hissediliyor. Bir anda dağılan yapı toparlanıyor, deploy tarafı da daha az naz yapıyor.
Dikkat Edilmesi Gereken Şeyler
Her şey güllük gülistanlık değil tabi (en azından benim deneyimim böyle). Hani bazen “tamamdır” diyorsunuz ama sonra köşeden bir paket çıkıp sizi hafifçe dürtüyor. Şu noktalara dikkat etmek lazım:
- Native module’ler:
node-gypile derlenen paketleriniz varsa (meselabcrypt,sharp) bundle işi biraz dallanıp budaklanır. Bu paketleriexternalolarak işaretleyip ayrıca kopyalamak gerekebilir. — ciddi fark yaratıyor - Dinamik require: Bazı paketler runtime’da string bazlı
requireyapar. Bundler bunları genelde yakalayamaz. Böyle bir durum varsa ilgili paketi manuel olarakexternal‘a ekleyin; yoksa sonradan uğraştırır. (bu kritik) - Lisans yükümlülükleri: Bundle sonrası tek dosyanın içinde epey OSS kod birikir. Lisans (MIT, Apache vb.) yükümlülüklerini bir NOTICE dosyasında toplamak iyi fikir olur. Bu taraf teknikten çok hukuk işi.
- Source map: Debug için source map üretin ama vsix’in içine koymayın. Ayrı bir artifact olarak sembol sunucusuna yüklemek daha temiz durur.
Sahada gördüğüm en yaygın hata: Ekipler bundling’e geçiyor ama
task.json‘dakiexecution.targetalanını güncellemeyi unutuyor. Task hâlâ eski entry point’i (meselaindex.js) arıyor, bulamıyor ve pipeline patlıyor. Build script’inizde bu alanı otomatik güncelleyin; unutma riskini iyice azaltmış olursunuz.
Evet.
Neyse uzatmayalım; burada kritik nokta şu: tek dosyaya toplama işi sadece temizlik sağlamıyor, aynı zamanda bakım yükünü de azaltıyor. Sız ne dersiniz?
Enterprise vs Küçük Ekip: Kim Nasıl Yaklaşmalı?
Bu iş, herkes için aynı kapıya çıkmıyor. Açık konuşayım, biraz duruma bağlı:
Küçük ekipseniz (birkaç task, düşük hacim): Bir günlük uğraşla bunu halledip geçersiniz, sonra sistem kendi kendine akıyor gibi olur. Görünür kazanç da şu oluyor: task marketplace’e yüklerken vsix küçülüyor, indirme biraz hızlanıyor. Devrim değil, ama hijyen tarafında baya iş görüyor.
Bi saniye — Enterprise seviyede (10+ task, günde binlerce çalıştırma): İşte burada tablo değişiyor. Bundle etmek, CI bir düşüneyim… altyapı maliyetini aşağı çekiyor; üstelik task güncellemeleri agent’lara daha hızlı ulaşıyor, deployment sırasında yaşanan o ufak sürtüşmeler de azalıyor. FinOps tarafında ölçülebilir bir karşılık veriyor, yanı laf olsun diye yapılmış bir iyileştirme değil.
Ha bu arada, bu tarz CI/CD iyileştirmelarını biraz daha geniş açıdan okumak isterseniz, Azure IaaS Maliyet Optimizasyonu: Sahadan FinOps Notları yazımda benzer sistematik yaklaşımları anlatmıştım. Küçük dokunuşlar var ya, bazen toplamda beklenmedik kadar fark yaratıyor.
Evet.
Alternatif Yaklaşımlar
esbuild tek yol değil tabiî. Hatta bazen ilk bakışta en parlak seçenek gibi dürüyor ama dur bir saniye — senaryoya göre başka araçlar da gayet iş çıkarabiliyor:
- webpack: Daha eski sayılır, evet biraz ağır kalabiliyor; ama ekosistemi oturmuş durumda. Zaten webpack config’ınız hazırsa sırf alışkanlığı bozmak için yeni maceraya gerek yok.
- rollup: Library bundling tarafında iyi gidiyor ama task paketleme için bazen fazla süslü kalıyor. Yanı olur mu? Olur. Ama her zaman en mantıklısı mı? Emin değilim.
- ncc (@vercel/ncc): GitHub Actions dünyasında sık görüyorsunuz. Node.js CLI işleri için özellikle pratik tasarlanmış; Azure DevOps task’larında da rahatça kullanılıyor ve çoğu zaman “kurduk çalıştı” hissi veriyor.
- tsup: TypeScript odaklı geliyor ve esbuild üstüne kurulu bir wrapper gibi davranıyor. Config tarafı sadeleşiyor; fazla ayar kurcalamak istemeyenler için fena değil.
Bence ilk bakılacak yer esbuild. Hızlı ilerliyor, config’i sade tutuyor ve TypeScript’i doğrudan yutuyor diyebilirim (ciddiyim). Ama ncc da boş değil; hatta bazı durumlarda size daha az karar verme yükü bırakıyor (bu da küçümsenecek şey değil). Denemekte fayda var.
Peki neden?
İlk adımda neye bakmalı?
Denemek istiyorsanız, bence bir hafta sonu ayırın ve şunları yapın:
- En büyük ve en sık çalışan task’ınızı seçin. Etkiyi orada daha net görürsünüz, boşuna küçük işlerle oyalanmayın.
- Mevcut task klasörünün boyutunu ve dosya sayısını ölçün. Baseline bu, yanı elinizdeki gerçek başlangıç noktası. — ciddi fark yaratıyor
- Bir dev/staging pipeline’da task’ın “Initialize job” süresini 10 çalıştırma boyunca ölçün. Ortalama alın, tek koşuya güvenmeyin; bazen sistem kafa karıştırıyor.
- esbuild ile bundle edin, yukarıdaki script’i temel alın. Hani şu hızlı paketleme işi var ya, burada baya iş görüyor. (bu kritik)
- Aynı pipeline’ı tekrar 10 kez çalıştırın. Sonra iki tarafı yan yana koyun, fark gerçekten var mı bakın.
- Sonuç iyiyse diğer task’lara da rollout planı yapın. Ama hemen her yere atlamayın; önce küçük bir grupta doğrulayın. (bence en önemlisi)
Bu arada, uçtan uca DevOps otomasyonu ve pipeline optimize etmeu tarafında agent-tabanlı yaklaşımlar da iyice konuşuluyor. Evet, biraz kalabalık — en azından ben öyle düşünüyorum — bir alan öldü ama işin aslı ilginç; konu sizi çekiyorsa Agent Harness: Kıskacı Büyütmek. Yetenekleri Kuşanmak yazısına da göz atabilirsiniz — farklı bir açıdan bakıyor, fena değil.
Kişisel Görüşüm
Açık konuşayım, bu tip “az bilinen ama ciddi fark yaratan” optimizasyonları ben de baya severim. Çünkü ekiplerin çoğu DevOps tarafında yeni araçların peşine düşüyor, yeni platform bakıyor, yeni pipeline kuruyor; ama eldeki setup’ı biraz kurcalayıp iyileştirmeyi unutuyor. Halbuki bazen mevcut sistemi %20 hızlandırmak, bambaşka bir yere taşınmaktan hem daha ucuz oluyor hem de daha az risk çıkarıyor.
Bir de şu var. 20 satırlık bir değişiklikle %98 iyileşme görünce insan önce seviniyor, sonra da “tamam da burası. Ne haldeymiş?” diye düşünüyor. Yanı birçok ekip task’larını gözden geçirmeden shipliyor ve bedelini her build’de, her deploy’da, hatta bazen gece yarısı telefon çaldığında ödüyor. Bu yazıyı okuduysanız, artık bunu bilmeden yapma şansınız pek kalmadı. Fark ettiyseniz, üstüne gitmek lazım.
Tabi esbuild tarafı da masal değil, önü da söyleyeyim. Bazı edge case’lerde saç baş yolduran şeyler çıkarabiliyor; özellikle dynamic import ve worker thread tarafında, hani “bu niye şimdi böyle davrandı?” dedirten anlar oluyor (bu konuda ikircikliyim). O yüzden production’a çıkmadan önce test coverage’ınızı ciddiye alın. Bundling sonrası mutlaka smoke test yapın.
Sıkça Sorulan Sorular
esbuild ile bundle edilen task’lar Azure DevOps Marketplace’te sorunsuz çalışır mı?
Evet, çalışıyor. Azure DevOps agent’ı aslında task’ın nasıl paketlendiğine hiç bakmıyor — sadece task.json‘daki execution.target‘ın gösterdiği JS dosyasını Node ile çalıştırıyor. Yanı tek dosya da olsa, node_modules ağacı da olsa, hiçbir fark etmiyor.
Bundle sonrası task’ımın boyutunu daha da küçültmek için ne yapabilirim?
Neyse, i̇lginç olan şu ki, esbuild’de minify: true flag’ını kullanın. Bir de drop: ['console', 'debugger'] ile production’da gereksiz kodları temizleyin — bence bu ikisi zaten olmazsa olmaz (bizzat test ettim). Mesela aws-sdk gibi büyük paketleri daha modüler alternatiflerle (v3 SDK gibi) değiştirmek de epey yardımcı oluyor. Ayrıca tree-shaking’i
Native module bağımlılığım var, bundle nasıl yapayım?
Native binary içeren paketleri esbuild’de external array’ine eklemeniz gerekiyor. Bunları bundle etmeye çalışmak yerine, task klasörüne ayrıca kopyalayın — böylece runtime’da require ile rahatlıkla bulabiliyor. Tecrübeme göre bir de şunu yapın: native bağımlılığı olmayan pure JS alternatifleri araştırın, çoğu zaman aslında bir alternatifi vardır.
Persistent (kalıcı) self-hosted agent kullanıyorum, yine de bundle etmeli mıyım?
İlk çalıştırmada büyük fark görüyorsunuz, sonrakilerde task cache’lendiği için etki biraz azalıyor. Ama her task güncellemesinde yeniden indiriliyor. Açıkçası, yine de yapın — hem cache invalidation senaryolarında hem de agent’ı yeniden kurduğunuzda kazançlı çıkıyorsunuz. Hani bir kerelik bir iş, neden yapmayasınız ki?
Bundle sonrası hata ayıklamak zorlaşır mı?
Biraz zorlaşıyor, evet — çünkü tüm kod tek dosyada birleşiyor. Bu yüzden esbuild’e sourcemap: true verip source map’i ayrı bir dosya olarak saklayın — itiraf edeyim, beklentimin üstündeydi —. Production hata log’larındaki satır numaralarını source map ile mapliyebiliyorsunuz. Bence en pratik çözüm bu. VS Code debugger’ı da source map ile lokal debug yapmanıza izin veriyor zaten.
Kaynaklar ve İleri Okuma
Shrinking Azure Pipeline task extensions using esbuild — Microsoft DevOps Blog
esbuild Resmî API Dokümantasyonu
Azure DevOps: Add a Custom Pipeline Task Extension
Bak şimdi, azure-pipelines-task-lib GitHub Repo
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.








Yorum gönder