CI/CD süreçlerinin yavaşlığı, bir yazılımcının gün içindeki odaklanma becerisini doğrudan baltalayan gizli bir yaşam kalitesi düşmanıdır. Her kod gönderdiğimde testlerin ve derleme (build) süreçlerinin bitmesini uzun uzun beklemek, beni bilgisayar başından uzaklaştırıp verimsiz bir döngüye sokuyordu. Kendi yan projelerimde bu süreyi kısaltmak için ciddi mesai harcadım.
Bu yazıda, doğrudan kendi uyguladığım ve pipeline sürelerini belirgin şekilde kısaltan CI/CD Pipeline’ında Build Cache Optimizasyonu yöntemlerini, somut konfigürasyonlar ve gerçek dünya tecrübelerimle anlatacağım. Amacım, teknik bir işi yaparken aslında kendi yaşam kalitemizi ve mental sağlığımızı nasıl koruyacağımızı göstermek.
CI/CD Pipeline’ında Build Cache Optimizasyonu Nedir ve Hayatımızı Nasıl Etkiler?
Yazılım geliştirme sürecinde “beklemek” en büyük motivasyon katilidir. Bir hata düzeltmesi yapıp repoya gönderdikten sonra pipeline’ın dakikalarca sıfırdan paket indirmesini izlemek, sadece sunucu faturasını kabartmaz; aynı zamanda geliştiricinin zihinsel akışını (flow state) kesintiye uğratır. CI/CD Pipeline’ında Build Cache Optimizasyonu, daha önce başarıyla tamamlanmış ara adımların sonuçlarını hafızada tutarak, sadece değişen kod parçalarını işlemeyi hedefler.
Kendi geliştirdiğim servislerde, ilk başlarda her deployment can sıkıcı şekilde uzun sürüyordu. Sadece tek satırlık bir log mesajı eklemek için bile uzun uzun beklemek beni işten soğutma noktasına getirmişti. Pratikte en çok zamanı tüketen adımlar genellikle şunlardır:
| Pipeline Adımı | Nerede Zaman Kaybedilir |
|---|---|
| Bağımlılıkların İndirilmesi | Cache yoksa her çalıştırmada sıfırdan indirme |
| Docker Image Build | Katman sırası yanlışsa tüm katmanların yeniden derlenmesi |
| Testlerin Çalıştırılması | Test ortamının/veritabanının her seferinde baştan kurulması |
Cache optimizasyonu bana sadece sunucu maliyetinden tasarruf ettirmedi, aynı zamanda gün içinde kahvemi soğutmadan deployment yapabilme özgürlüğü verdi. Eğer siz de her “git push” sonrası mutfağa gidip çay demliyorsanız, orada bir mühendislik problemi var demektir. Daha önce ele aldığım VPS disk doluluk sorunları yazımda da belirttiğim gibi, optimize edilmeyen her geçici dosya ve cache katmanı zamanla tüm sistemi tıkayan bir çığa dönüşür.
Yöntem 1: Docker Layer Caching (DLC) ile Boşa Geçen Dakikaları Kurtarmak
Docker imajı oluştururken yapılan en büyük hata, kaynak kodları kopyalamadan önce paket yönetim dosyalarını (package.json, go.mod, requirements.txt) doğru sırayla işleme almamaktır. Docker, her satırı (instruction) bir katman (layer) olarak kaydeder. Eğer kaynak kodlarınızı paket listesinden önce imaja kopyalarsanız, yazdığınız tek bir boşluk karakteri bile tüm paketlerin sıfırdan indirilmesine neden olur.
Kendi projelerimde kullandığım ve build süresini dramatik şekilde düşüren optimize edilmiş bir Dockerfile yapısını aşağıda paylaşıyorum. Bu yapı, Docker’ın katman önbellekleme mekanizmasını sonuna kadar sömürür:
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
# Sadece paket tanımlarını kopyalıyoruz
COPY package.json package-lock.json ./
# Bağımlılıkları kuruyoruz (Bu katman paketler değişmedikçe cache'ten okunur)
RUN npm ci --prefer-offline --no-audit
# Şimdi kaynak kodları kopyalayabiliriz
COPY . .
# Derleme adımı
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "dist/main.js"]
Buradaki kritik numara COPY package.json package-lock.json ./ satırıdır. Eğer siz doğrudan COPY . . yapıp ardından npm ci derseniz, projedeki herhangi bir .ts veya .js dosyası değiştiğinde Docker tüm node_modules klasörünü internetten sıfırdan çekmeye başlar.
Yöntem 2: Paket Yöneticisi Cache’lerini (Node_Modules, Cargo, Go) Kalıcı Hale Getirmek
Docker katmanları lokalde çok iyi çalışır ancak GitHub Actions, GitLab CI veya self-hosted runner kullanıyorsanız, her pipeline çalışmasında temiz bir sanal makine (clean slate) ayağa kalkar. Bu durumda Docker local cache’i kaybolur. Çözüm, CI/CD sağlayıcısının sunduğu global önbellek (cache) mekanizmasını kullanarak paket yöneticilerinin indirdiği dosyaları pipeline’lar arasında taşımaktır.
Özellikle Rust (cargo) veya Go projelerinde derleme süreleri CPU limitleri yüzünden can sıkıcı boyutlara ulaşabilir. GitHub Actions üzerinde çalışan bir iş akışında (workflow) paket cache’ini nasıl kilitlediğimi gösteren YAML konfigürasyonunu aşağıda inceleyebilirsiniz:
# .github/workflows/ci.yml
name: Continuous Integration
on:
push:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# lock dosyalarının hash'ine göre cache anahtarı oluşturuyoruz
- name: Cache Node Modules
uses: actions/cache@v4
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci --prefer-offline --no-audit
Bu konfigürasyonda hashFiles('**/package-lock.json') ifadesi hayati önem taşır. Eğer bağımlılıklarda hiçbir değişiklik yoksa, GitHub Actions gidip ~/.npm klasörünü saniyeler içinde geri yükler. npm ci --prefer-offline parametresi ise npm’in internete gitmeden önce yerel önbelleğe bakmasını zorunlu kılar. Bu sayede dakikalarca süren paket kurulum aşaması saniyeler düzeyine iner.
Yöntem 3: Multi-Stage Builds ve Runner-Local Cache Entegrasyonu
Eğer kendi sunucunuzda (bare-metal veya VPS üzerinde) GitLab Runner veya Jenkins koşturuyorsanız, internet bant genişliğiniz ve disk yazma hızınız pipeline performansını doğrudan etkiler. Bu senaryoda bulut tabanlı cache servisleri yerine runner’ın üzerinde duran lokal disk alanını “mount” ederek kullanmak çok daha mantıklıdır.
Özellikle veritabanı ağırlıklı projelerde, PostgreSQL şemalarının taşınması (migration) ve backend testlerinin ayağa kalkması her seferinde sıfırdan veritabanı kurulmasını gerektirir. Bu tür durumlarda lokal runner üzerinde docker soketini paylaşarak (/var/run/docker.sock) ve lokal bir dizini cache olarak bağlayarak sorunu çözebilirsiniz.
Aşağıdaki Docker Compose ve CI runner entegrasyonu örneği, lokal disk üzerindeki önbelleği nasıl paylaştıracağınızı gösteriyor:
# docker-compose.ci.yml
version: '3.8'
services:
builder:
image: docker:24-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Local runner üzerindeki cache dizinini container içine bağlıyoruz
- /tmp/ci-cache/.npm:/root/.npm
- /tmp/ci-cache/go-build:/root/.cache/go-build
environment:
- DOCKER_BUILDKIT=1
command: docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t my-app:latest .
Bu yöntem sayesinde, harici bir bulut depolama alanına (S3 vb.) upload/download trafiği yapmadan, doğrudan runner’ın SSD disk hızıyla cache okuması yapıyoruz. Özellikle Türkiye lokasyonlu sunucularda yurtdışı çıkış hızlarının dalgalı olduğu dönemlerde bu lokal disk caching stratejisi hayat kurtarır.
Optimizasyon Sonrası Metrikler: Yatırımın Geri Dönüşü (ROI)
Yapılan her optimizasyonun somut bir çıktısı olmalıdır. “Sistem hızlandı” demek yerine bunu metriklerle kanıtlamak mühendislik disiplininin bir gereğidir. Bu yüzden optimizasyon öncesi ve sonrası süreleri ölçmek, kazanımı hem süre hem de finansal açıdan takip etmek önemlidir.
Bu üç yöntem birlikte uygulandığında, pipeline başına kazanılan dakikalar ay sonunda ciddi bir toplama ulaşır. Kazancı görmek için şu basit eşitliği kendi rakamlarınızla doldurmanız yeterli:
- Pipeline başına kazanç × aylık çalıştırma sayısı = kurtarılan sunucu zamanı. Bu doğrudan CI/CD sunucu faturasına yansır.
- Aynı süre × geliştirici sayısı = kurtarılan odaklanma zamanı. Asıl paha biçilemez kalem budur.
Sunucu maliyetinden gelen tasarruf çoğu zaman mütevazı kalır; ancak geri kazanılan geliştirici zamanı paha biçilemezdir. Geliştiricinin pipeline beklerken odağının dağılması, bağlam değiştirme (context switching) maliyetiyle birleştiğinde şirkete ve projeye faturası katlanarak büyür. Daha önce Yazılım mimarisi ve organizasyonel akış yazımda da değindiğim gibi, süreçleri optimize etmek aslında insan kaynağını en verimli şekilde kullanmanın en akıllıca yoludur.
Build Cache Yönetiminde Kaçınmanız Gereken Tuzaklar ve Sınırlar
Her güzel şeyin olduğu gibi build cache kullanımının da bazı yan etkileri ve sınırları vardır. Önbellekleme mekanizmalarına körü körüne güvenmek, bazen “lokalde çalışıyor ama sunucuda patladı” sendromunun en büyük sebebidir.
Karşılaştığım en sinsi sorunlardan biri, bozuk (corrupted) bir cache katmanının sürekli olarak yeni derlemeleri sabote etmesiydi. Bu gibi durumlarla başa çıkmak için şu kuralları asla es geçmiyorum:
- Cache Sürümleme (Cache Busting): Bağımlılık dosyalarınız değişmese bile ara sıra cache’i tamamen sıfırlamak gerekir. Pipeline tetiklerken bir ortam değişkeni (env) ile cache versiyonunu değiştirebilmelisiniz (örn:
CACHE_VERSION=v2). - Gizli Dosyaların Önbelleğe Sızması:
.envdosyaları, SSL sertifikaları veya SSH key’ler gibi hassas verilerin asla cache dizinlerine dahil edilmediğinden emin olun..dockerignoreve.gitignoredosyalarını güncel tutun. - Disk Alanı Yönetimi: Runner lokalindeki cache dizinleri zamanla şişer.
systemdtimer’ları ile haftalık olarak eski cache klasörlerini temizleyen küçük bir cron job çalıştırmak sistem sağlığı için elzemdir.
Sonraki adım olarak, kendi projelerinizdeki en yavaş çalışan pipeline’ı seçip işe başlayabilirsiniz. İlk olarak Dockerfile optimizasyonunu yapın, ardından paket yöneticisi cache’ini devreye alın. Farkı ilk derlemeden sonra doğrudan hissedeceksiniz.