GitLab CI/CD’de Artifact ve Cache Yönetimi
GitLab CI/CD pipeline’larınızda her build sıfırdan başlıyorsa, bağımlılıklar her seferinde internetten indiriliyor ve testler gereksiz yere tekrarlanıyorsa, artifact ve cache yönetimini düzgün yapılandırmamışsınız demektir. Bu iki kavram, pipeline sürelerinizi dramatik biçimde kısaltabilir ve ekibinizin verimliliğini ciddi ölçüde artırabilir. Gelin bu konuyu derinlemesine inceleyelim.
Artifact ve Cache Arasındaki Temel Fark
Çoğu kişi artifact ile cache’i birbirine karıştırıyor. Aslında ikisi tamamen farklı amaçlara hizmet ediyor.
Artifact, bir job’ın ürettiği ve diğer job’lara veya kullanıcılara aktarılması gereken dosyalardır. Örneğin derleme sonucunda ortaya çıkan binary dosyalar, test raporları, coverage raporu veya deploy edilecek paket bunların hepsine artifact denir. Artifact’lar pipeline boyunca job’lar arasında taşınır ve varsayılan olarak GitLab UI üzerinden indirilebilir.
Cache ise job’lar arasında paylaşılan ama pipeline’ın çıktısı olmayan dosyalardır. En klasik örnek node_modules klasörü veya pip’in indirdiği paketlerdir. Cache’in amacı şudur: bağımlılıkları her seferinde internetten çekmek yerine bir önceki run’dan kalan dosyaları yeniden kullanmak.
Pratik kural olarak şunu aklınızda tutun: “Bu dosya bir sonraki stage’de lazım mı? Artifact. Bu dosyayı her build’de yeniden indirmek istiyor muyum? Cache.”
Cache Yapılandırması
Temel Cache Kullanımı
En basit haliyle bir cache tanımı şu şekilde görünür:
build:
stage: build
image: node:18
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm install
- npm run build
Burada CI_COMMIT_REF_SLUG branch adını cache key olarak kullanıyor. Bu sayede main branch’i kendi cache’ine sahip olurken feature/login branch’i ayrı bir cache kullanır.
Cache Key Stratejileri
Cache key seçimi son derece kritik. Yanlış key seçerseniz ya hiç cache hit almaz, ya da eski bağımlılıklarla çalışırsınız.
Dosya bazlı cache key en akıllıca yaklaşım. package-lock.json değişmediği sürece node_modules değişmez, dolayısıyla bu dosyanın hash’ini key olarak kullanmak mantıklı:
install_deps:
stage: prepare
image: node:18
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci
Bu yapıda package-lock.json değişmediği sürece GitLab mevcut cache’i kullanır, dosya değişince otomatik olarak yeni bir cache oluşturur. Hem güvenli hem de verimli.
Python projeleri için benzer yaklaşım:
install_python_deps:
stage: prepare
image: python:3.11
cache:
key:
files:
- requirements.txt
prefix: python-deps
paths:
- .pip-cache/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
script:
- pip install -r requirements.txt
prefix parametresi farklı Python versiyonları veya farklı ortamlar için aynı requirements.txt hash’ine farklı cache’ler atamanıza olanak tanır.
Global Cache Tanımı
Tüm job’larda ortak bir cache kullanmak istiyorsanız, .gitlab-ci.yml‘ın en üst seviyesinde cache tanımlayabilirsiniz:
default:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull
stages:
- build
- test
- deploy
build:
stage: build
image: gradle:7.6-jdk17
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
script:
- gradle build
unit_test:
stage: test
image: gradle:7.6-jdk17
script:
- gradle test
Burada önemli bir nokta var: policy parametresi. pull-push varsayılan davranış olup cache’i hem çeker hem de günceller. pull ise sadece mevcut cache’i çeker, güncelleme yapmaz. Test job’larında genellikle pull kullanmak daha mantıklı çünkü test, bağımlılık değiştirmez.
Cache Temizleme ve Sıfırlama
Bazen cache bozulur veya eski bağımlılıklar sorun çıkarır. GitLab’da cache’i sıfırlamanın birkaç yolu var.
İlki CACHE_FALLBACK_KEY veya prefix değiştirmek. İkincisi ve çok daha pratik olanı ise CI/CD değişkenine tarihi eklemek:
cache:
key: "${CI_COMMIT_REF_SLUG}-v2"
paths:
- node_modules/
v2‘yi v3 yapmanız yeterli, tüm cache’ler sıfırlanmış olur.
GitLab UI üzerinden de cache’i temizleyebilirsiniz: Project > Build > Pipelines > Clear Runner Caches.
Artifact Yapılandırması
Temel Artifact Kullanımı
Bir job’ın ürettiği dosyaları sonraki stage’e taşımak için artifact kullanmanız gerekir. Aksi halde runner’lar arası dosya paylaşımı mümkün değildir.
build:
stage: build
image: maven:3.9-eclipse-temurin-17
artifacts:
paths:
- target/*.jar
expire_in: 1 week
script:
- mvn clean package -DskipTests
deploy_staging:
stage: deploy
dependencies:
- build
script:
- echo "Deploying $(ls target/*.jar)"
- scp target/*.jar user@staging-server:/apps/
expire_in parametresi artifact’ların ne kadar süre saklanacağını belirler. Sonsuz saklamamak önemli, disk dolabilir.
Artifact expire_in Değerleri
- never: Hiç silinmez (dikkatli kullanın)
- 1 hour: Bir saat
- 3 days: Üç gün
- 1 week: Bir hafta
- 2 months: İki ay
Test Raporları ve JUnit Entegrasyonu
GitLab, test sonuçlarını natively parse edebilir. Bu sayede Merge Request üzerinde başarısız testleri doğrudan görebilirsiniz:
run_tests:
stage: test
image: python:3.11
script:
- pip install pytest pytest-cov
- pytest tests/ --junitxml=junit-report.xml --cov=src --cov-report=xml:coverage.xml
artifacts:
when: always
reports:
junit: junit-report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- junit-report.xml
- coverage.xml
expire_in: 30 days
when: always çok önemli. Varsayılan davranış sadece başarılı job’larda artifact yüklemektir. Ama test başarısız olduğunda da raporu görmek istiyorsunuz, dolayısıyla always kullanmalısınız.
Artifact Bağımlılıkları
Varsayılan olarak her job, önceki tüm stage’lerin artifact’larını çeker. Bu gereksiz yere bant genişliği harcar. dependencies veya needs ile bunu kısıtlayabilirsiniz:
stages:
- build
- test
- package
- deploy
build_backend:
stage: build
artifacts:
paths:
- dist/backend/
expire_in: 1 day
build_frontend:
stage: build
artifacts:
paths:
- dist/frontend/
expire_in: 1 day
test_backend:
stage: test
dependencies:
- build_backend
script:
- echo "Sadece backend artifact'ı var"
package_all:
stage: package
dependencies:
- build_backend
- build_frontend
script:
- ls dist/
Hiç artifact istemiyorsanız dependencies: [] yazmanız yeterli.
Gerçek Dünya Senaryoları
Senaryo 1: Node.js Monorepo Pipeline
Büyük bir e-ticaret firmasında çalıştığınızı düşünün. Monorepo içinde birden fazla servis var ve her birinin bağımlılıkları farklı. Hem cache’i optimize etmek hem de artifact’ları doğru yönetmek gerekiyor:
stages:
- install
- lint
- test
- build
- deploy
variables:
NODE_VERSION: "18"
ARTIFACTS_DIR: "artifacts"
.node_cache: &node_cache
cache:
key:
files:
- package-lock.json
prefix: "node-${NODE_VERSION}"
paths:
- node_modules/
- .npm/
policy: pull
install_dependencies:
stage: install
image: node:${NODE_VERSION}
cache:
key:
files:
- package-lock.json
prefix: "node-${NODE_VERSION}"
paths:
- node_modules/
- .npm/
policy: pull-push
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths:
- node_modules/
expire_in: 1 day
lint:
stage: lint
image: node:${NODE_VERSION}
needs:
- install_dependencies
<<: *node_cache
script:
- npm run lint
artifacts:
when: on_failure
paths:
- lint-results/
expire_in: 3 days
build_production:
stage: build
image: node:${NODE_VERSION}
needs:
- lint
<<: *node_cache
script:
- npm run build:prod
artifacts:
paths:
- dist/
expire_in: 1 week
when: on_success
Bu örnekte YAML anchor (&node_cache) kullanarak cache konfigürasyonunu tekrar etmekten kaçındık. needs ile DAG (Directed Acyclic Graph) pipeline oluşturarak paralel çalışmayı maksimize ettik.
Senaryo 2: Docker Image Build ve Registry
Docker build süreçlerinde layer cache son derece değerli. GitLab ile Docker BuildKit cache’ini şöyle entegre edebilirsiniz:
build_docker:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_BUILDKIT: "1"
IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
CACHE_IMAGE: "$CI_REGISTRY_IMAGE:cache"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
docker build
--cache-from $CACHE_IMAGE
--build-arg BUILDKIT_INLINE_CACHE=1
--tag $IMAGE_TAG
--tag $CACHE_IMAGE
.
- docker push $IMAGE_TAG
- docker push $CACHE_IMAGE
artifacts:
reports:
dotenv: build.env
after_script:
- echo "IMAGE_TAG=$IMAGE_TAG" >> build.env
Burada $CACHE_IMAGE tag’ını ayrı bir image olarak saklıyoruz. Bir sonraki build bu image’ı --cache-from ile kullanarak Docker layer cache’ini devreye sokuyor. Özellikle büyük dependency layer’ları olan imajlarda build süresini yüzde altmış-yetmişe kadar düşürebilirsiniz.
Senaryo 3: Multi-Stage Mobile Build
iOS/Android CI süreçlerinde artifact yönetimi kritik. Build çıktısı bir sonraki job tarafından kullanılmak üzere saklanmalı:
stages:
- build
- sign
- distribute
build_android:
stage: build
image: reactnativecommunity/react-native-android:latest
cache:
key:
files:
- yarn.lock
prefix: "rn-android"
paths:
- node_modules/
- ~/.gradle/caches/
- ~/.gradle/wrapper/
script:
- yarn install --frozen-lockfile
- cd android && ./gradlew assembleRelease
artifacts:
name: "android-build-${CI_COMMIT_SHORT_SHA}"
paths:
- android/app/build/outputs/apk/release/*.apk
expire_in: 3 days
when: on_success
sign_apk:
stage: sign
image: openjdk:17
dependencies:
- build_android
script:
- echo $KEYSTORE_BASE64 | base64 -d > release.keystore
- |
zipalign -v -p 4
android/app/build/outputs/apk/release/app-release-unsigned.apk
app-release-aligned.apk
- |
apksigner sign
--ks release.keystore
--ks-pass pass:$KEYSTORE_PASSWORD
--out app-release-signed.apk
app-release-aligned.apk
artifacts:
name: "android-signed-${CI_COMMIT_SHORT_SHA}"
paths:
- app-release-signed.apk
expire_in: 1 week
İleri Seviye Konfigürasyonlar
Artifact Exclude Kullanımı
Bazen bir dizinin tamamını artifact olarak almak ama bazı dosyaları dışarıda bırakmak istersiniz:
build:
artifacts:
paths:
- dist/
exclude:
- dist/**/*.map
- dist/**/*.d.ts
expire_in: 1 week
Source map ve TypeScript declaration dosyaları genellikle dağıtım için gereksizdir ama dist/ klasöründe bulunurlar. exclude ile bunları artifact dışında tutabilirsiniz.
Cache Policy ve Koşullu Cache
Bazı durumlarda cache’i sadece belirli koşullarda güncellemek isteyebilirsiniz. Örneğin sadece main branch’te cache’i güncelleyen, diğer branch’lerde sadece okuyan bir yapı:
build:
stage: build
cache:
key: "shared-deps"
paths:
- vendor/
policy: $CACHE_POLICY
variables:
CACHE_POLICY: pull
script:
- composer install --prefer-dist
update_cache:
stage: build
cache:
key: "shared-deps"
paths:
- vendor/
policy: pull-push
script:
- composer install --prefer-dist
rules:
- if: $CI_COMMIT_BRANCH == "main"
Paralel Job’larda Cache Koordinasyonu
Paralel testler çalıştırırken cache key çakışmasını önlemek önemli:
test:
stage: test
image: ruby:3.2
parallel: 4
cache:
key: "ruby-gems-${CI_COMMIT_REF_SLUG}"
paths:
- vendor/bundle
policy: pull
variables:
CI_NODE_INDEX: $CI_NODE_INDEX
CI_NODE_TOTAL: $CI_NODE_TOTAL
script:
- bundle install --path vendor/bundle
- bundle exec rspec --format progress $(bundle exec rspec --dry-run --format json | jq -r '.examples[].id' | awk "NR % ${CI_NODE_TOTAL} == ${CI_NODE_INDEX}")
Burada tüm paralel job’lar aynı cache key’i kullanıyor ama policy: pull ile sadece okuyor. Cache’i güncelleyen ayrı bir job olması daha verimli.
Pipeline Performans Optimizasyonları
Artifact Boyutunu Küçültmek
Büyük artifact’lar pipeline süresini doğrudan etkiler. Upload ve download süresi ciddi bir yük oluşturabilir. Bazı pratik önlemler:
- Sadece gerekli dosyaları artifact olarak tanımlayın.
dist/yerinedist/*/.jsgibi spesifik path kullanın. - expire_in süresini gereğinden uzun koymayın. Günlük build artifact’ları için 3-7 gün yeterli.
- Untrack edilmiş dosyaları .gitignore’a eklemek artifact’tan çıkarmaz, explicit exclude kullanın.
- Binary dosyaları compress edin script içinde, artifact’ı zip olarak gönderin.
Cache Hit Oranını İzlemek
GitLab job loglarında cache durumunu görebilirsiniz. “Checking cache for…” satırlarına bakın. “Successfully extracted cache” görüyorsanız hit alıyorsunuz. “No URL provided, cache will not be downloaded” veya “WARNING: cache_policy is ‘pull’ but no cache is available” görüyorsanız sorun var demektir.
Cache hit oranını artırmak için:
- Cache key’i çok sık değişen değişkenler üzerine kurmayın
${CI_PIPELINE_ID}gibi her pipeline’da değişen değerleri key’de kullanmaktan kaçının- Dosya bazlı key (
key.files) tercih edin
Distributed Runner’larda Cache Davranışı
Eğer birden fazla runner kullanıyorsanız, cache varsayılan olarak sadece aynı runner üzerinde paylaşılır. Farklı runner’lar arasında cache paylaşmak için S3, GCS veya Azure Blob depolama yapılandırmanız gerekir.
config.toml üzerinde runner cache konfigürasyonu:
[[runners]]
name = "production-runner"
url = "https://gitlab.example.com"
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "gitlab-runner-cache"
BucketLocation = "eu-west-1"
AuthenticationType = "iam"
Shared = true yapıldığında tüm runner’lar aynı cache bucket’ını kullanır. Bu enterprise ortamlarda büyük fark yaratır.
Sık Yapılan Hatalar
- Cache ile artifact’ı karıştırmak: Build output’unu cache’e koymak, dependency’leri artifact’a almak sık görülen bir hata. Cache kaybolabilir, artifact güvenilirdir.
- Her zaman pull-push kullanmak: Test job’larında
pull-pushkullanmak gereksiz cache upload’larına neden olur. Test bağımlılık değiştirmez.
- expire_in vermemek: Artifact’lara expire vermemek GitLab storage’ını şişirir. Özellikle self-hosted instance’larda disk dolma sorununa yol açar.
- Çok geniş artifact path’leri:
paths: [.]gibi tüm workspace’i artifact olarak almak hem yavaş hem de gereksizdir.
- Cache key olarak branch adı kullanmak ama farklı OS’larda çalışmak: Windows ve Linux runner’ları aynı cache’i paylaşamaz, key’e platform bilgisi ekleyin.
Sonuç
GitLab CI/CD’de artifact ve cache yönetimi, pipeline performansının bel kemiğini oluşturuyor. Temel ayrımı netleştirmek gerekirse: artifact’lar job’lar arası veri taşır ve pipeline çıktısıdır; cache ise build süresini kısaltmak için yeniden kullanılan geçici dosyalardır.
Pratikte yapmanız gereken adımlar şunlar: Her projenin bağımlılık yöneticisine uygun dosya bazlı cache key tanımlayın, artifact’lara expire_in süresi ekleyin, test job’larında policy: pull kullanın ve gereksiz artifact bağımlılıklarını dependencies ile sınırlayın. Bu dört adımı uyguladığınızda çoğu pipeline’ın yüzde otuz ile elli arasında hızlandığını göreceksiniz.
Dağıtık runner ortamında çalışıyorsanız S3 tabanlı shared cache konfigürasyonu yapmayı ihmal etmeyin. Aksi halde cache stratejinizin hiçbir etkisi olmayabilir. Son olarak job loglarını düzenli izleyin ve cache hit oranlarını takip edin. Sayılar sizi doğru yönlendirecektir.
