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 ama payment-service de 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:

  • changes direktifi ile yalnızca değişen servislerin build edilmesini sağladık.
  • include direktifi ile her servisin CI konfigürasyonunu kendi dizininde tutarak yönetilebilirlik sağladık.
  • needs ile DAG pipeline kurarak job’lar arasında gereksiz bekleme sürelerini ortadan kaldırdık.
  • dotenv artifact’ları ile job’lar arasında değişiklik bilgisi aktardık.
  • Cache stratejilerini her teknoloji stack’ine göre özelleştirdik.
  • interruptible ve resource_group ile 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir