GitLab ile Monorepo Yönetimi ve Stratejiler
Üç yıl önce 40 kişilik bir ekiple 12 ayrı repository’yi yönetmeye çalışırken bir sabah işe geldiğimde 7 farklı Slack mesajı beni bekliyordu. Hepsinin ortak noktası: “Hangi versiyon hangi servisle uyumlu?” Dependency cehennemi denen şey buydu işte. O günden sonra monorepo geçişini ciddiye aldım.
Monorepo, adının aksine “her şeyi bir yere tıkıştır” felsefesi değil. Doğru yapılandırılmış bir monorepo, ekipler arası görünürlüğü artırır, dependency yönetimini merkezileştirir ve CI/CD pipeline’larını anlamlı hale getirir. GitLab ise bu iş için gerçekten iyi düşünülmüş araçlara sahip; ancak bu araçları bilmeden monorepo kurmak, araba yerine traktörle Formula 1 yapmaya benziyor.
Monorepo Nedir, Ne Değildir
Monorepo; birden fazla projenin, servisin veya kütüphanenin tek bir Git repository’sinde barındırılması anlamına gelir. Google, Meta, Microsoft gibi şirketlerin yıllardır uyguladığı bu yaklaşım, küçük ve orta ölçekli ekipler için de giderek daha anlamlı hale geliyor.
Monorepo’nun yanlış anlaşılan boyutları:
- Monorepo, monolit değildir. Mikroservis mimarisi monorepo içinde gayet güzel yaşar.
- Monorepo, her şeyi aynı yapar anlamına gelmez. Her servis kendi build ve deploy döngüsüne sahip olabilir.
- Monorepo, küçük ekipler için verimsizdir yanılgısı doğru değildir. 5 kişilik ekipler bile çok repository yönetiminde ciddi zaman kaybeder.
Öte yandan monorepo’nun gerçek maliyetleri de var: repository büyüklüğü artar, CI süresi optimize edilmezse çığ gibi büyür ve Git performansı dikkatli yönetilmezse acı çektirir.
GitLab’ın Monorepo İçin Sunduğu Temel Araçlar
GitLab, monorepo yönetimi için birkaç kritik özellik sunar. Bunların başında rules ve changes direktifleri gelir. Bu ikili olmadan monorepo CI’ı yönetilemez.
Dizin Bazlı Pipeline Tetikleme
En temel ihtiyaç şu: Yalnızca değişen servisi build et ve deploy et. Aksi halde 20 servisli bir monorepo’da her commit tüm pipeline’ı tetikler ve CI makineleri yanar.
# .gitlab-ci.yml
stages:
- build
- test
- deploy
# Auth servisi - sadece auth/ altında değişiklik varsa çalışır
build:auth-service:
stage: build
script:
- cd services/auth
- docker build -t $CI_REGISTRY_IMAGE/auth:$CI_COMMIT_SHA .
rules:
- changes:
- services/auth/**/*
- shared/libs/**/* # shared lib değişirse de tetikle
when: on_success
# Payment servisi
build:payment-service:
stage: build
script:
- cd services/payment
- docker build -t $CI_REGISTRY_IMAGE/payment:$CI_COMMIT_SHA .
rules:
- changes:
- services/payment/**/*
- shared/libs/**/*
when: on_success
Burada dikkat edilmesi gereken nokta: shared/libs/*/ her iki serviste de izleniyor. Paylaşılan kütüphane değiştiğinde bu kütüphaneye bağımlı tüm servisler yeniden build alıyor. Bu, dependency graph’ı manuel olarak CI YAML içinde ifade etmek demek; biraz zahmetli ama kontrol sizde.
Child Pipeline Mimarisi
Gerçek dünya monorepo’larında tek bir .gitlab-ci.yml dosyası kaosa dönüşür. Çözüm: her servisin kendi pipeline dosyasına sahip olması ve ana pipeline’ın bunları orkestre etmesi.
# .gitlab-ci.yml (ana pipeline - orkestratör)
stages:
- trigger
trigger:auth:
stage: trigger
trigger:
include: services/auth/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- services/auth/**/*
trigger:payment:
stage: trigger
trigger:
include: services/payment/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- services/payment/**/*
# services/auth/.gitlab-ci.yml (servis bazlı pipeline)
stages:
- build
- test
- deploy
build:
stage: build
image: golang:1.21
script:
- go build -o bin/auth-service ./cmd/auth/
artifacts:
paths:
- bin/auth-service
test:unit:
stage: test
image: golang:1.21
script:
- go test ./... -v -coverprofile=coverage.out
- go tool cover -html=coverage.out -o coverage.html
coverage: '/coverage: d+.d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
deploy:staging:
stage: deploy
script:
- kubectl set image deployment/auth-service auth=$CI_REGISTRY_IMAGE/auth:$CI_COMMIT_SHA -n staging
environment:
name: staging
only:
- main
strategy: depend önemli. Bu direktif olmadan ana pipeline, child pipeline’ların tamamlanmasını beklemeden başarılı sayılır. Production’da bu tür bir “gözden kaçma” gerçekten can sıkıcı sonuçlar doğurabilir.
Repository Dizin Yapısı Stratejileri
Monorepo’da dizin yapısı, ekibin zihinsel modelini yansıtmalı. İki popüler yaklaşım var:
Servis Odaklı Yapı
Mikroservis ağırlıklı ekipler için uygundur:
monorepo/
├── services/
│ ├── auth/
│ │ ├── .gitlab-ci.yml
│ │ ├── Dockerfile
│ │ ├── cmd/
│ │ └── internal/
│ ├── payment/
│ │ ├── .gitlab-ci.yml
│ │ ├── Dockerfile
│ │ └── src/
│ └── notification/
│ ├── .gitlab-ci.yml
│ └── src/
├── shared/
│ ├── libs/
│ │ ├── logger/
│ │ └── config/
│ └── proto/
├── infrastructure/
│ ├── terraform/
│ ├── helm/
│ └── kubernetes/
├── docs/
└── .gitlab-ci.yml
Domain Odaklı Yapı
DDD (Domain Driven Design) uygulayan ekipler için:
monorepo/
├── domains/
│ ├── user-management/
│ │ ├── auth-service/
│ │ ├── profile-service/
│ │ └── shared/
│ ├── commerce/
│ │ ├── product-service/
│ │ ├── cart-service/
│ │ └── payment-service/
│ └── notification/
├── platform/
│ ├── api-gateway/
│ └── service-mesh/
└── .gitlab-ci.yml
Hangi yapıyı seçeceğiniz organizasyonunuzun şekline göre değişir. Önemli olan tutarlılık; ekibinizin yarısı servis odaklı, yarısı domain odaklı düşünüyorsa sorun yaşarsınız.
Branch Stratejisi ve Merge Request Akışı
Monorepo’da branch stratejisi kritik. Tek bir merge request birden fazla servisi etkileyebileceğinden code review süreci dikkatli kurgulanmalı.
CODEOWNERS ile Servis Sahipliği
# CODEOWNERS dosyası (repository root'unda)
# Global sahipler
* @devops-team
# Servis bazlı sahiplik
/services/auth/ @auth-team @security-team
/services/payment/ @payment-team @security-team
/services/notification/ @notification-team
/shared/libs/ @platform-team
/infrastructure/ @devops-team
/docs/ @tech-writers
Bu dosya sayesinde GitLab, bir merge request’te hangi dosyaların değiştiğine bakarak ilgili ekipleri otomatik reviewer olarak atar. 20 servisli bir monorepo’da bu özellik olmadan code review kaosa döner.
Merge Request Template’leri
Her servis için farklı MR template’i kullanabilirsiniz:
# .gitlab/merge_request_templates/service-change.md
## Değişiklik Özeti
<!-- Yapılan değişikliği kısaca açıklayın -->
## Etkilenen Servisler
- [ ] auth-service
- [ ] payment-service
- [ ] notification-service
## Test Edildi mi?
- [ ] Unit testler geçiyor
- [ ] Integration testler geçiyor
- [ ] Staging ortamında doğrulandı
## Backward Compatibility
- [ ] API değişikliği yok
- [ ] Database migration var (migration dosyası eklendi mi?)
- [ ] Shared lib değişikliği diğer servisleri etkiliyor
## Deployment Notları
<!-- Özel deployment adımları varsa belirtin -->
Shared Library Versiyonlama
Monorepo’nun en hassas noktalarından biri paylaşılan kütüphanelerin versiyonlanması. İki yaklaşım var:
Yaklaşım 1: Lock-step versioning – Tüm servisler her zaman shared library’nin en güncel halini kullanır. Basit ama riskli; bir breaking change tüm servisleri etkiler.
Yaklaşım 2: Pinned versioning – Her servis kullandığı shared library versiyonunu explicit belirtir. Daha güvenli ama yönetmesi zor.
Pratikte çoğu ekip hibrit bir yaklaşım benimser. Örneğin logger gibi stabil kütüphaneler için lock-step, API client gibi değişken kütüphaneler için pinned.
# scripts/check-shared-deps.sh
# Shared lib değişikliklerinden etkilenen servisleri tespit eder
#!/bin/bash
CHANGED_FILES=$(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_COMMIT_SHA)
AFFECTED_SERVICES=()
for file in $CHANGED_FILES; do
if [[ $file == shared/libs/* ]]; then
LIB_NAME=$(echo $file | cut -d'/' -f3)
echo "Değişen shared lib: $LIB_NAME"
# Bu library'yi kullanan servisleri bul
DEPENDENTS=$(grep -rl ""$LIB_NAME"" services/ --include="*.go" --include="package.json" 2>/dev/null | xargs -I{} dirname {} | sort -u)
for service in $DEPENDENTS; do
AFFECTED_SERVICES+=("$service")
echo "Etkilenen servis: $service"
done
fi
done
if [ ${#AFFECTED_SERVICES[@]} -gt 0 ]; then
echo "AFFECTED_SERVICES=${AFFECTED_SERVICES[*]}" >> report.env
fi
GitLab’da Büyük Repository Performans Optimizasyonu
Repository büyüdükçe git clone süreleri can yakar. GitLab ve Git’in sunduğu çözümler:
Partial Clone ve Sparse Checkout
# Tüm repository yerine sadece ihtiyaç duyulan dizini clone et
git clone --filter=blob:none --sparse https://gitlab.company.com/org/monorepo.git
cd monorepo
# Sadece ilgili servis dizinini checkout al
git sparse-checkout set services/auth shared/libs
# Çalışmak istediğiniz alanı genişletmek isterseniz
git sparse-checkout add services/payment
# CI/CD ortamında kullanım - .gitlab-ci.yml
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 10 # Sadece son 10 commit'i al
build:auth-service:
variables:
GIT_SPARSE_CHECKOUT_ENABLED: "true"
GIT_SPARSE_CHECKOUT_PATH: "services/auth shared/libs"
script:
- cd services/auth
- make build
GIT_DEPTH: 10 ayarı özellikle dikkat gerektiriyor. Bazı CI scriptleri git log veya git diff ile eski commit’lere erişmeye çalışır; depth küçük tutulursa bu işlemler başarısız olur. Değişiklik algılama scriptleri yazdıysanız bu ayarı test ortamında iyice sınayın.
LFS Kullanımı
Binary dosyalar (görseller, ML modelleri, büyük test fixture’ları) doğrudan Git’te saklanmamalı:
# Git LFS kurulumu ve konfigürasyonu
git lfs install
# Büyük dosya tiplerini LFS'e yönlendir
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.zip"
git lfs track "*.model"
git lfs track "testdata/*.sql"
# .gitattributes dosyasını commit'le
git add .gitattributes
git commit -m "chore: configure Git LFS for binary assets"
Monorepo’da Release Yönetimi
Her servis kendi release döngüsüne sahip olduğunda versiyonlama ve release notları ayrı bir zorluk haline gelir. GitLab’ın release özelliğini ve semantic versioning’i birleştiren bir yaklaşım:
# .gitlab-ci.yml - release job'u
release:auth-service:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Auth service release $AUTH_VERSION"
release:
name: "auth-service-$AUTH_VERSION"
description: "Auth Service Release $AUTH_VERSION"
tag_name: "auth/$AUTH_VERSION"
assets:
links:
- name: "Docker Image"
url: "$CI_REGISTRY_IMAGE/auth:$AUTH_VERSION"
rules:
- if: $CI_COMMIT_TAG =~ /^auth/vd+.d+.d+$/
# scripts/release.sh - Servis bazlı tag oluşturma
#!/bin/bash
SERVICE=$1
VERSION=$2
if [ -z "$SERVICE" ] || [ -z "$VERSION" ]; then
echo "Kullanım: ./release.sh <servis-adı> <versiyon>"
echo "Örnek: ./release.sh auth v1.2.3"
exit 1
fi
# CHANGELOG güncelle
echo "## $SERVICE $VERSION - $(date +%Y-%m-%d)" >> services/$SERVICE/CHANGELOG.md
git log --oneline $(git describe --tags --match="$SERVICE/*" --abbrev=0)..HEAD -- services/$SERVICE >> services/$SERVICE/CHANGELOG.md
# Tag oluştur ve push et
git tag -a "$SERVICE/$VERSION" -m "Release $SERVICE $VERSION"
git push origin "$SERVICE/$VERSION"
echo "$SERVICE $VERSION etiketi oluşturuldu ve push edildi."
Gerçek Dünya Senaryosu: E-ticaret Monorepo’su
Müşterilerden birinde 8 mikroservisi olan bir e-ticaret platformunu monorepo’ya geçirdik. Geçiş sonrası karşılaştığımız somut kazanımlar ve sorunlar:
Kazanımlar:
- Cross-service refactoring süresi %60 azaldı. Artık tek bir MR ile API kontratını ve tüm consumer’ları aynı anda güncelleyebiliyoruz.
- “Bu değişiklik hangi servisi etkiler?” sorusunun cevabı
git log --all -- shared/ile anında geliyor. - Yeni geliştirici onboarding süresi kısaldı; tek clone, tüm ekosistemi görme imkanı.
Sorunlar ve çözümleri:
- İlk ay CI süreleri 3 katına çıktı.
rules: changesdirektifini doğru yapılandırarak %70 azalttık. - Junior geliştiriciler yanlış dizinde çalışmaya başladı.
pre-commithook’ları ile bunu önledik. git blamevegit logçıktıları gürültülü hale geldi.git log -- services/auth/gibi path-spesifik komutları ekibe öğrettik.
# .git/hooks/pre-commit - Yanlış dizin koruması
#!/bin/bash
CURRENT_DIR=$(pwd)
STAGED_FILES=$(git diff --cached --name-only)
# Farklı servislere ait dosyaların aynı commit'te olup olmadığını kontrol et
SERVICES_TOUCHED=$(echo "$STAGED_FILES" | grep "^services/" | cut -d'/' -f2 | sort -u | wc -l)
if [ "$SERVICES_TOUCHED" -gt 2 ]; then
echo "UYARI: Bu commit 2'den fazla servisi etkiliyor ($SERVICES_TOUCHED servis)."
echo "Etkilenen servisler:"
echo "$STAGED_FILES" | grep "^services/" | cut -d'/' -f2 | sort -u
echo ""
echo "Devam etmek için ENTER'a basın, iptal için Ctrl+C..."
read
fi
Pipeline Optimizasyon Taktikleri
Monorepo’da CI maliyetini düşürmek için uyguladığım birkaç somut taktik:
- Cache agresif kullan: Her servisin bağımlılıklarını ayrı cache key’i ile sakla.
- Test paralelleştirme: GitLab’ın
paralleldirektifini kullanarak test suitlerini böl. - Fail fast: Kritik testleri önce çalıştır, uzun süren integration testleri sona bırak.
- Scheduled pipeline: Nightly tam test, PR’da sadece etkilenen alanların testi.
# Paralel test örneği
test:payment:
stage: test
parallel: 4
script:
- cd services/payment
- go test ./... -run "TestGroup_${CI_NODE_INDEX}" -count=1
rules:
- changes:
- services/payment/**/*
Sonuç
Monorepo geçişi bir araç değişikliği değil, çalışma kültürü değişikliği. GitLab bu yolculuğu kolaylaştıran güçlü araçlara sahip; rules/changes ile akıllı pipeline tetikleme, child pipeline mimarisi, CODEOWNERS ile otomatik review atama ve partial clone ile performans optimizasyonu bunların başında geliyor.
Ancak şunu net söylemek gerekir: Monorepo her organizasyon için doğru seçim değil. Ekipler arasında düşük koordinasyon olan, teknik standartları birbirinden çok farklı olan veya tamamen bağımsız yaşam döngüleri olan projeler için çoklu repository daha sağlıklı olabilir.
Eğer geçiş yapacaksanız küçük başlayın. İki veya üç yakın ilişkili servisle başlayıp süreçleri oturtun, sonra genişletin. Ve CI’yi en başından optimize etmeyi ihmal etmeyin; büyük bir monorepo’da optimize edilmemiş pipeline, ekip motivasyonunu hızla eritir.
