Monorepo Pipeline Yönetimi: GitLab CI/CD Rehberi
Büyük bir yazılım projesini tek repoda yönetmek kulağa basit gibi gelir, ancak CI/CD tarafında işler çok hızlı karmaşıklaşabilir. Monorepo yapısında her commit’te tüm projeyi build etmek hem zaman hem de kaynak israfına yol açar. GitLab CI/CD ise bu sorunu çözmek için oldukça güçlü araçlar sunar. Bu yazıda, gerçek dünya senaryolarına dayanan bir monorepo pipeline kurulumunu adım adım ele alacağız.
Monorepo Nedir ve Neden Zorlayıcıdır?
Monorepo, birden fazla servisin veya uygulamanın tek bir Git reposunda tutulduğu yapıdır. Örneğin bir e-ticaret platformunda frontend, backend-api, payment-service, notification-service gibi bileşenler aynı repoda yaşayabilir.
Bu yaklaşımın avantajları tartışılmaz: kod paylaşımı kolay olur, dependency yönetimi merkezi hale gelir, refactoring daha az risklidir. Ancak CI/CD tarafında şu sorunlar ortaya çıkar:
- Her push’ta her şeyi build etmek: Sadece
frontend‘i değiştirdiniz amapayment-servicede yeniden build ediliyor. Bu pipeline’ları gereksiz yere uzatır. - Paralel job yönetimi: Hangi servis önce build edilmeli, hangisi hangisine bağımlı?
- Artifact ve cache karmaşası: Farklı servislerin artifact’ları birbirine karışabilir.
- Test izolasyonu: Bir servisin testleri diğerini etkilememeli.
GitLab CI/CD’nin rules, changes, needs, include gibi özellikleri bu sorunları çözmek için biçilmiş kaftandır.
Proje Yapısını Tanımlamak
Yazı boyunca şu monorepo yapısını kullanacağız:
my-platform/
├── .gitlab-ci.yml
├── frontend/
│ ├── src/
│ ├── package.json
│ └── Dockerfile
├── backend-api/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── payment-service/
│ ├── src/
│ ├── requirements.txt
│ └── Dockerfile
├── notification-service/
│ ├── src/
│ ├── go.mod
│ └── Dockerfile
└── shared/
└── libs/
Her servisin kendi teknoloji stack’i var: React, Java Spring Boot, Python, Go. Bu ayrışıklık gerçek dünya monorepo’larında çok sık karşılaşılan bir durum.
Ana Pipeline Dosyasını Kurmak
GitLab CI/CD’de monorepo yönetiminin temel taşı include direktifi ve changes kuralıdır. Ana .gitlab-ci.yml dosyasını aşağıdaki gibi yapılandırıyoruz:
# .gitlab-ci.yml
stages:
- detect
- build
- test
- security
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
REGISTRY: registry.gitlab.com/mycompany/my-platform
include:
- local: 'frontend/.gitlab-ci-frontend.yml'
- local: 'backend-api/.gitlab-ci-backend.yml'
- local: 'payment-service/.gitlab-ci-payment.yml'
- local: 'notification-service/.gitlab-ci-notification.yml'
detect-changes:
stage: detect
image: alpine:3.18
script:
- |
echo "Changed services:"
git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA |
grep -E '^(frontend|backend-api|payment-service|notification-service)/' |
cut -d'/' -f1 | sort -u
only:
- merge_requests
- main
- develop
Bu yapıda her servis kendi CI dosyasını yönetir, ana dosya sadece orchestration görevini üstlenir.
Servis Bazlı CI Dosyaları
Frontend Pipeline
# frontend/.gitlab-ci-frontend.yml
.frontend-rules:
rules:
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
changes:
- frontend/**/*
- shared/libs/**/*
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- frontend/**/*
- shared/libs/**/*
frontend-build:
stage: build
image: node:20-alpine
extends: .frontend-rules
cache:
key:
files:
- frontend/package-lock.json
paths:
- frontend/node_modules/
before_script:
- cd frontend
- npm ci --cache .npm --prefer-offline
script:
- npm run build
- npm run lint
artifacts:
paths:
- frontend/dist/
expire_in: 1 hour
frontend-test:
stage: test
image: node:20-alpine
extends: .frontend-rules
needs:
- job: frontend-build
artifacts: true
cache:
key:
files:
- frontend/package-lock.json
paths:
- frontend/node_modules/
before_script:
- cd frontend
script:
- npm run test:unit -- --coverage
- npm run test:e2e:ci
coverage: '/Liness*:s*(d+.?d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: frontend/coverage/cobertura-coverage.xml
junit: frontend/test-results/junit.xml
frontend-docker:
stage: deploy
image: docker:24
extends: .frontend-rules
services:
- docker:24-dind
needs:
- job: frontend-build
artifacts: true
- job: frontend-test
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
docker build
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
--build-arg VCS_REF=$CI_COMMIT_SHA
-t $REGISTRY/frontend:$CI_COMMIT_SHA
-t $REGISTRY/frontend:latest
frontend/
- docker push $REGISTRY/frontend:$CI_COMMIT_SHA
- docker push $REGISTRY/frontend:latest
only:
- main
Buradaki kritik nokta .frontend-rules template’i. Bu template, changes direktifi sayesinde yalnızca frontend/ veya shared/libs/ dizinlerinde değişiklik olduğunda job’ları tetikler. shared/libs buraya dahil edildi çünkü paylaşılan kütüphane değiştiğinde frontend de yeniden build edilmeli.
Backend API Pipeline
# backend-api/.gitlab-ci-backend.yml
.backend-rules:
rules:
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
changes:
- backend-api/**/*
- shared/libs/**/*
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- backend-api/**/*
backend-build:
stage: build
image: maven:3.9-eclipse-temurin-17
extends: .backend-rules
cache:
key: "$CI_PROJECT_ID-maven-$CI_COMMIT_REF_SLUG"
paths:
- backend-api/.m2/
before_script:
- cd backend-api
script:
- mvn package -DskipTests -Dmaven.repo.local=.m2
artifacts:
paths:
- backend-api/target/*.jar
expire_in: 2 hours
backend-test:
stage: test
image: maven:3.9-eclipse-temurin-17
extends: .backend-rules
needs:
- job: backend-build
artifacts: true
services:
- name: postgres:15
alias: test-db
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
SPRING_DATASOURCE_URL: jdbc:postgresql://test-db:5432/testdb
before_script:
- cd backend-api
script:
- mvn test -Dmaven.repo.local=.m2
- mvn jacoco:report -Dmaven.repo.local=.m2
artifacts:
reports:
junit: backend-api/target/surefire-reports/TEST-*.xml
paths:
- backend-api/target/site/jacoco/
needs direktifine dikkat edin. backend-test, backend-build job’ının tamamlanmasını bekler ve artifact’larını kullanır. Bu sayede stage sıralamasından bağımsız olarak dependency graph kurulur.
Paralel Build Stratejisi
Büyük monorepo’larda tüm servislerin sıralı build edilmesi pipeline süresini katlar. GitLab’ın needs özelliği ile DAG (Directed Acyclic Graph) tabanlı pipeline kurabilirsiniz:
# Paralel build örneği - tüm servislerin aynı anda build edilmesi
build-all:
stage: build
trigger:
strategy: depend
parallel:
matrix:
- SERVICE: frontend
IMAGE: node:20-alpine
BUILD_CMD: "cd frontend && npm ci && npm run build"
- SERVICE: backend-api
IMAGE: maven:3.9-eclipse-temurin-17
BUILD_CMD: "cd backend-api && mvn package -DskipTests"
- SERVICE: payment-service
IMAGE: python:3.11-slim
BUILD_CMD: "cd payment-service && pip install -r requirements.txt"
- SERVICE: notification-service
IMAGE: golang:1.21
BUILD_CMD: "cd notification-service && go build ./..."
image: $IMAGE
script:
- eval $BUILD_CMD
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Bu yaklaşım matrix stratejisi kullanarak dört servisi aynı anda build eder. Ancak dikkatli olun: eğer servisler birbirine bağımlıysa önce bağımlılık grafiğini çizmelisiniz.
Değişiklik Algılama ile Akıllı Pipeline
Sadece changes direktifine güvenmek bazen yeterli olmaz. Özellikle develop veya main branch’e yapılan merge sonrası ilk commit olmayan push’larda CI_COMMIT_BEFORE_SHA değeri 0000000 olabilir. Daha güvenilir bir değişiklik algılama scripti yazalım:
#!/bin/bash
# scripts/detect-changes.sh
set -e
CHANGED_SERVICES=""
SERVICES=("frontend" "backend-api" "payment-service" "notification-service" "shared")
# İlk commit veya force push durumunu handle et
if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "Initial commit or force push detected. Building all services."
for service in "${SERVICES[@]}"; do
echo "CHANGED_${service^^//-/_}=true" >> changed.env
done
exit 0
fi
# Değişen dosyaları bul
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA 2>/dev/null ||
git diff --name-only HEAD~1 HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
for service in "${SERVICES[@]}"; do
if echo "$CHANGED_FILES" | grep -q "^${service}/"; then
CHANGED_SERVICES="$CHANGED_SERVICES $service"
SERVICE_VAR="CHANGED_${service^^}"
SERVICE_VAR="${SERVICE_VAR//-/_}"
echo "${SERVICE_VAR}=true" >> changed.env
echo "Service changed: $service"
fi
done
if [ -z "$CHANGED_SERVICES" ]; then
echo "No service-specific changes detected."
echo "CHANGED_NONE=true" >> changed.env
fi
cat changed.env
Bu scripti pipeline’da şöyle kullanırsınız:
detect-changes:
stage: detect
image: alpine/git:latest
script:
- chmod +x scripts/detect-changes.sh
- ./scripts/detect-changes.sh
artifacts:
reports:
dotenv: changed.env
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE != "schedule"'
frontend-conditional-build:
stage: build
image: node:20-alpine
needs:
- job: detect-changes
artifacts: true
rules:
- if: '$CHANGED_FRONTEND == "true"'
- if: '$CHANGED_SHARED == "true"'
script:
- cd frontend && npm ci && npm run build
dotenv artifact tipi, bir job’dan diğerine environment variable geçirmenin en temiz yoludur. detect-changes job’u changed.env dosyasını üretir, sonraki job’lar bu variable’ları okur.
Security Scanning Entegrasyonu
GitLab Ultimate veya Free tier’da kısmi olarak kullanılabilen security template’lerini monorepo’ya entegre etmek için aşağıdaki yapıyı kullanabilirsiniz:
# security/.gitlab-ci-security.yml
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
sast:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp, node_modules, vendor"
dependency_scanning:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
DS_EXCLUDED_PATHS: "$CI_PROJECT_DIR/frontend/node_modules"
secret_detection:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
Security job’larını her servisin pipeline’ından ayrı tutmak iyi bir pratiktir. Bu sayede bir servis değişmese bile security scan yine de çalışabilir.
Environment’a Göre Deploy Stratejisi
Monorepo’da deploy yönetimi karmaşıklaşabilir. Hangi servis hangi environment’a ne zaman deploy edilmeli? Bunu yönetmek için environment-specific rules kullanalım:
# deploy/.gitlab-ci-deploy.yml
.deploy-template:
image: bitnami/kubectl:latest
before_script:
- kubectl config use-context $KUBE_CONTEXT
- kubectl version --client
deploy-frontend-staging:
extends: .deploy-template
stage: deploy
environment:
name: staging/frontend
url: https://staging.myplatform.com
variables:
KUBE_CONTEXT: $KUBE_CONTEXT_STAGING
script:
- |
kubectl set image deployment/frontend
frontend=$REGISTRY/frontend:$CI_COMMIT_SHA
-n staging
- kubectl rollout status deployment/frontend -n staging --timeout=300s
needs:
- job: frontend-docker
artifacts: false
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
changes:
- frontend/**/*
deploy-frontend-production:
extends: .deploy-template
stage: deploy
environment:
name: production/frontend
url: https://myplatform.com
variables:
KUBE_CONTEXT: $KUBE_CONTEXT_PROD
script:
- |
kubectl set image deployment/frontend
frontend=$REGISTRY/frontend:$CI_COMMIT_SHA
-n production
- kubectl rollout status deployment/frontend -n production --timeout=300s
needs:
- job: frontend-docker
artifacts: false
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- frontend/**/*
when: manual
when: manual ile production deploy’ları bir onay adımına bağladık. Staging’e otomatik, production’a manual deploy yaklaşımı çoğu ekip için ideal bir denge sunar.
Cache Optimizasyonu
Monorepo’larda cache yönetimi kritiktir. Her servis farklı bir cache stratejisi gerektirir:
# Cache stratejileri
# Node.js için içerik tabanlı cache key
.node-cache:
cache:
key:
files:
- frontend/package-lock.json
prefix: node-v20
paths:
- frontend/node_modules/
- frontend/.npm/
policy: pull-push
# Maven için branch bazlı fallback
.maven-cache:
cache:
- key: "maven-$CI_COMMIT_REF_SLUG"
paths:
- backend-api/.m2/
policy: pull-push
- key: "maven-develop"
paths:
- backend-api/.m2/
policy: pull
# Go modules cache
.go-cache:
cache:
key:
files:
- notification-service/go.sum
paths:
- notification-service/.cache/go/
variables:
GOPATH: "$CI_PROJECT_DIR/notification-service/.cache/go"
policy: pull-push
Maven cache’de iki katmanlı strateji dikkat çekiyor. Önce branch-specific cache denenecek, bulunamazsa develop branch’inin cache’i kullanılacak. Bu yaklaşım özellikle yeni feature branch’lerinde boş cache problemini çözer.
Pipeline Performans Metrikleri
Pipeline sürelerinizi takip etmek için GitLab’ın built-in analytics özelliklerini kullanabilirsiniz. Ancak daha detaylı metrik toplamak istiyorsanız şu yaklaşımı deneyin:
pipeline-metrics:
stage: detect
image: curlimages/curl:latest
script:
- |
# Pipeline başlangıç zamanını kaydet
START_TIME=$(date +%s)
echo "PIPELINE_START_TIME=$START_TIME" >> metrics.env
# Değişen servis sayısını hesapla
CHANGED_COUNT=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA |
cut -d'/' -f1 | sort -u |
grep -E '^(frontend|backend-api|payment-service|notification-service)$' |
wc -l)
echo "CHANGED_SERVICE_COUNT=$CHANGED_COUNT" >> metrics.env
# Prometheus push gateway'e gönder (opsiyonel)
if [ -n "$PROMETHEUS_PUSHGATEWAY_URL" ]; then
cat <<EOF | curl --data-binary @- $PROMETHEUS_PUSHGATEWAY_URL/metrics/job/gitlab-pipeline
gitlab_pipeline_changed_services{project="$CI_PROJECT_NAME",branch="$CI_COMMIT_REF_NAME"} $CHANGED_COUNT
EOF
fi
artifacts:
reports:
dotenv: metrics.env
rules:
- if: '$CI_COMMIT_BRANCH'
Sık Karşılaşılan Sorunlar ve Çözümleri
Monorepo pipeline yönetiminde tekrar tekrar karşılaşılan bazı durumlar var:
changes direktifi merge request’lerde beklenmedik davranış gösteriyor: Bu genellikle CI_COMMIT_BEFORE_SHA değerinin yanlış hesaplanmasından kaynaklanır. Merge request pipeline’larında GitLab, değişiklikleri target branch ile karşılaştırır, bu yüzden changes genellikle doğru çalışır. Ancak scheduled pipeline’larda changes direktifi yok sayılır ve job her zaman çalışır.
Tüm stage’ler atlama problemi: Eğer hiçbir servis değişmediyse GitLab “This pipeline is empty” hatası verebilir. Bu durumu engellemek için her zaman çalışacak bir sentinel job ekleyin:
pipeline-health-check:
stage: detect
image: alpine:3.18
script:
- echo "Pipeline started successfully"
- echo "Commit: $CI_COMMIT_SHA"
- echo "Branch: $CI_COMMIT_REF_NAME"
- echo "Triggered by: $CI_PIPELINE_SOURCE"
rules:
- if: '$CI_COMMIT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Shared library değişikliklerinde hangi servislerin etkilendiğini bulmak: shared/libs değiştiğinde tüm servislerin yeniden build edilmesi gerekebilir. Bunu changes ile handle etmek için her servis kuralında shared/libs/*/ path’ini de ekleyin. Ancak daha sofistike bir yaklaşım için bağımlılık grafiği çizen bir script yazabilirsiniz.
Pipeline quota aşımı: GitLab.com’da aylık CI/CD dakika limitiniz varsa monorepo build’leri bunu çok hızlı tüketir. Her job için resource_group kullanarak concurrent build sayısını sınırlayabilir, interruptible: true ile eski pipeline’ları otomatik iptal edebilirsiniz:
frontend-build:
interruptible: true
resource_group: frontend-$CI_COMMIT_REF_SLUG
Merge Request Pipeline’larını Optimize Etmek
Geliştiriciler için en kritik geri bildirim döngüsü merge request pipeline’larıdır. Bu pipeline’ların hızlı ve güvenilir olması üretkenliği doğrudan etkiler:
# Merge request özel konfigürasyonu
.mr-only-fast-checks:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
lint-all-services:
extends: .mr-only-fast-checks
stage: build
image: alpine:3.18
parallel:
matrix:
- SERVICE: frontend
LINT_IMAGE: node:20-alpine
LINT_CMD: "cd frontend && npm ci --silent && npm run lint"
- SERVICE: backend-api
LINT_IMAGE: maven:3.9-eclipse-temurin-17
LINT_CMD: "cd backend-api && mvn checkstyle:check"
image: $LINT_IMAGE
script:
- eval $LINT_CMD
allow_failure:
exit_codes: 1
Sonuç
Monorepo pipeline yönetimi ilk kurulumda karmaşık görünse de doğru yapılandırıldığında hem geliştirici deneyimini hem de kaynak kullanımını ciddi ölçüde iyileştirir. Bu yazıda ele aldığımız yaklaşımları özetleyecek olursak:
changesdirektifi ile yalnızca değişen servislerin build edilmesini sağladık.includedirektifi ile her servisin CI konfigürasyonunu kendi dizininde tutarak yönetilebilirlik sağladık.needsile DAG pipeline kurarak job’lar arasında gereksiz bekleme sürelerini ortadan kaldırdık.dotenvartifact’ları ile job’lar arasında değişiklik bilgisi aktardık.- Cache stratejilerini her teknoloji stack’ine göre özelleştirdik.
interruptibleveresource_groupile paralel pipeline çakışmalarını önledik.
En önemli tavsiye: pipeline’ınızı küçük adımlarla büyütün. Önce temel changes kurallarını ekleyin, çalıştığını doğrulayın, sonra cache optimizasyonuna geçin. Her yeni özelliği production’a almadan önce bir test reposunda deneyin. GitLab CI/CD’nin dokümantasyonu oldukça iyi hazırlanmış, özellikle rules sözdizimi için resmi dokümana sık sık dönmenizi öneririm.
Pipeline süreleriniz düştükçe ve geliştiricileriniz “neden bu kadar uzun sürüyor?” sorusunu sormayı bıraktıkça doğru yolda olduğunuzu anlarsınız.
