DAG Pipeline: GitLab CI/CD ile Paralel Job Yönetimi

GitLab CI/CD pipeline’larıyla ciddi ölçekli projeler yönetiyorsanız, er ya da geç şu soruyla yüzleşiyorsunuz: “Bu joblar neden sıra sıra bekliyor ki, aynı anda çalışabilirler.” İşte tam burada DAG (Directed Acyclic Graph) pipeline konsepti devreye giriyor ve hayatınızı ciddi ölçüde kolaylaştırıyor.

DAG Pipeline Nedir?

Klasik GitLab CI/CD pipeline’larında işler stage’lere göre çalışır. Bir stage tamamlanmadan bir sonrakine geçilmez. Bu yaklaşım sade ve anlaşılır olsa da büyük projelerde ciddi bir zaman kaybına yol açar. Örneğin test stage’inizde birbirinden bağımsız 5 farklı job varsa, hepsi paralel çalışır. Ama deploy stage’inizdeki bir job, sadece bir test jobuna bağımlı olsa bile tüm test stage’inin bitmesini beklemek zorundadır.

DAG pipeline bu problemi çözer. DAG, yönlü döngüsüz çizge anlamına gelir ve matematiksel olarak şunu ifade eder: Joblar arasında bağımlılık ilişkileri tanımlarsınız, bir job yalnızca kendisine bağımlı olduğu joblar bittiğinde çalışmaya başlar. Stage sıralaması değil, iş bazlı bağımlılık mantığı geçerlidir.

GitLab 12.2 sürümüyle hayatımıza giren needs keyword’ü bu yapının temelidir. needs ile bir job, kendi stage’i gelmeden önce bile çalışmaya başlayabilir, yeter ki bağımlı olduğu işler tamamlanmış olsun.

Klasik Pipeline vs DAG Pipeline

Farkı somutlaştırmak için bir senaryo düşünelim. Microservice mimarisi kullanan bir e-ticaret uygulaması geliştiriyorsunuz. Projenizde şu bileşenler var:

  • auth-service: Kimlik doğrulama servisi
  • product-service: Ürün yönetim servisi
  • order-service: Sipariş servisi (hem auth hem product’a bağımlı)
  • frontend: React uygulaması

Klasik yaklaşımda pipeline’ınız şöyle görünür:

build stage --> test stage --> deploy stage
(tüm build'ler biter) --> (tüm testler biter) --> (deploy başlar)

Bu yaklaşımda auth-service build ve test’i 2 dakikada bitmiş olsa bile, order-service build’i 8 dakika sürüyorsa deploy 8 dakika bekler. DAG’da ise auth-service ve product-service hazır olur olmaz order-service için gerekli adımlar başlar.

Temel needs Kullanımı

Hemen pratik bir örnekle başlayalım. En basit DAG pipeline yapısı şöyle kurulur:

stages:
  - build
  - test
  - deploy

build:auth:
  stage: build
  script:
    - echo "Auth service build ediliyor"
    - docker build -t auth-service:$CI_COMMIT_SHA ./auth-service

build:product:
  stage: build
  script:
    - echo "Product service build ediliyor"
    - docker build -t product-service:$CI_COMMIT_SHA ./product-service

build:order:
  stage: build
  script:
    - echo "Order service build ediliyor"
    - docker build -t order-service:$CI_COMMIT_SHA ./order-service

test:auth:
  stage: test
  needs:
    - build:auth
  script:
    - echo "Auth service testleri çalışıyor"
    - pytest auth-service/tests/

test:product:
  stage: test
  needs:
    - build:product
  script:
    - echo "Product service testleri çalışıyor"
    - pytest product-service/tests/

test:order:
  stage: test
  needs:
    - build:order
    - test:auth
    - test:product
  script:
    - echo "Order service testleri çalışıyor"
    - pytest order-service/tests/

deploy:staging:
  stage: deploy
  needs:
    - test:auth
    - test:product
    - test:order
  script:
    - echo "Staging ortamına deploy ediliyor"
    - kubectl apply -f k8s/staging/

Burada dikkat edilmesi gereken nokta: test:auth job’u, build:auth bitmesiyle birlikte hemen başlar. test:product beklemiyor. test:order ise hem auth hem product testlerinin bitmesini bekliyor çünkü bu servislerin hazır olmasına ihtiyaç duyuyor.

Artifact Transferi ile needs Kullanımı

needs keyword’ünün çok işe yarayan bir özelliği var: bağımlı olduğunuz job’dan artifact alabilirsiniz. Varsayılan olarak needs ile bağlandığınız job’ların artifact’ları otomatik indirilir. Bunu kontrol etmek için artifacts parametresini kullanırsınız:

build:frontend:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

test:frontend:
  stage: test
  needs:
    - job: build:frontend
      artifacts: true
  script:
    - ls dist/  # build artifact'ı burada mevcut
    - npm run test:e2e

deploy:frontend:
  stage: deploy
  needs:
    - job: build:frontend
      artifacts: true
    - job: test:frontend
      artifacts: false  # test artifact'larına ihtiyacımız yok
  script:
    - aws s3 sync dist/ s3://my-bucket/

artifacts: false kullanmak pipeline performansını artırır çünkü gereksiz dosya transferleri önlenir. Özellikle büyük artifact’lar söz konusu olduğunda bu optimizasyon belirgin fark yaratır.

Gerçek Dünya Senaryosu: Monorepo Pipeline

Monorepo yapısında birden fazla servis barındıran bir projede DAG pipeline’ın gücü tam anlamıyla ortaya çıkıyor. Diyelim ki şu yapıya sahibiz:

/
├── services/
│   ├── api-gateway/
│   ├── user-service/
│   ├── notification-service/
│   └── payment-service/
├── shared/
│   └── common-lib/
└── infra/
    └── terraform/

Bu yapı için kapsamlı bir DAG pipeline yazalım:

stages:
  - validate
  - build
  - unit-test
  - integration-test
  - security-scan
  - deploy-staging
  - smoke-test
  - deploy-production

# Shared library önce build edilmeli
build:common-lib:
  stage: build
  script:
    - cd shared/common-lib
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - shared/common-lib/target/*.jar
    expire_in: 2 hours

# Servisler paralel build, hepsi common-lib'e bağımlı
build:user-service:
  stage: build
  needs:
    - job: build:common-lib
      artifacts: true
  script:
    - cd services/user-service
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - services/user-service/target/*.jar
    expire_in: 2 hours

build:notification-service:
  stage: build
  needs:
    - job: build:common-lib
      artifacts: true
  script:
    - cd services/notification-service
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - services/notification-service/target/*.jar
    expire_in: 2 hours

build:payment-service:
  stage: build
  needs:
    - job: build:common-lib
      artifacts: true
  script:
    - cd services/payment-service
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - services/payment-service/target/*.jar
    expire_in: 2 hours

# Unit testler servis build'leri biter bitmez başlar
unit-test:user-service:
  stage: unit-test
  needs:
    - job: build:user-service
      artifacts: true
  script:
    - cd services/user-service
    - mvn test

unit-test:payment-service:
  stage: unit-test
  needs:
    - job: build:payment-service
      artifacts: true
  script:
    - cd services/payment-service
    - mvn test
    - mvn jacoco:report
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: services/payment-service/target/site/jacoco/jacoco.xml

# Integration test hem user hem payment service'e bağımlı
integration-test:checkout-flow:
  stage: integration-test
  needs:
    - unit-test:user-service
    - unit-test:payment-service
    - job: build:notification-service
      artifacts: true
  services:
    - postgres:14
    - redis:7
  script:
    - ./scripts/run-integration-tests.sh checkout

needs ile Pipeline Optimizasyonu

DAG’ın asıl gücü, stage sınırlarını aşabilmesinde yatıyor. Bir job, kendi stage’inden önceki herhangi bir job’a needs ile bağlanabilir. Bunu kullanarak ciddi zaman kazanımları elde edebilirsiniz:

stages:
  - build
  - test
  - security
  - deploy

build:api:
  stage: build
  script:
    - docker build -t api:$CI_COMMIT_SHA .
    - docker push registry.example.com/api:$CI_COMMIT_SHA

# Security scan, test stage'ini beklemeden build biter bitmez başlar
security:container-scan:
  stage: security
  needs:
    - build:api
  script:
    - trivy image registry.example.com/api:$CI_COMMIT_SHA

security:sast:
  stage: security
  needs: []  # Hiçbir job'a bağımlı değil, hemen başlar
  script:
    - semgrep --config=auto src/

test:unit:
  stage: test
  needs:
    - build:api
  script:
    - pytest tests/unit/

test:integration:
  stage: test
  needs:
    - build:api
  script:
    - pytest tests/integration/

# Deploy hem security hem testlerin bitmesini bekler
deploy:staging:
  stage: deploy
  needs:
    - security:container-scan
    - security:sast
    - test:unit
    - test:integration
  script:
    - helm upgrade --install api-staging ./charts/api 
        --set image.tag=$CI_COMMIT_SHA 
        --namespace staging

needs: [] kullanımına dikkat edin. Boş array ile bir job’u hiçbir bağımlılığa bağlamıyorsunuz, yani pipeline başlar başlamaz çalışmaya başlıyor. SAST gibi kaynak kodu analiz eden araçlar için bu yaklaşım mantıklı.

needs ile Opsiyonel Job Bağımlılıkları

Bazen bağımlı olduğunuz job koşullu olarak çalışabilir ya da çalışmayabilir. optional: true parametresi bu durumu yönetmenizi sağlar:

build:docker-image:
  stage: build
  only:
    - main
    - develop
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .

test:smoke:
  stage: test
  needs:
    - job: build:docker-image
      optional: true
  script:
    - |
      if docker image inspect myapp:$CI_COMMIT_SHA &>/dev/null; then
        echo "Docker image mevcut, smoke test çalıştırılıyor"
        docker run --rm myapp:$CI_COMMIT_SHA ./smoke-test.sh
      else
        echo "Docker image yok, temel testler çalıştırılıyor"
        ./basic-smoke-test.sh
      fi

Feature branch’lerde build:docker-image çalışmıyor ama test:smoke çalışmasına devam edebiliyor. optional: true olmasaydı, test:smoke bağımlı job bulunamadığı için başlamazdı.

Paralel Matrix ile DAG Kombinasyonu

parallel:matrix ile DAG’ı birleştirdiğinizde çok güçlü bir yapı elde ediyorsunuz. Birden fazla ortam ya da konfigürasyon için aynı job’u paralel çalıştırabilirsiniz:

stages:
  - build
  - test
  - deploy

build:service:
  stage: build
  parallel:
    matrix:
      - SERVICE: [auth, product, order, notification]
  script:
    - cd services/$SERVICE
    - docker build -t $SERVICE:$CI_COMMIT_SHA .
    - docker push registry.example.com/$SERVICE:$CI_COMMIT_SHA
  artifacts:
    reports:
      dotenv: services/$SERVICE/build.env

test:service:
  stage: test
  parallel:
    matrix:
      - SERVICE: [auth, product, order, notification]
  needs:
    - job: build:service
      parallel:
        matrix:
          - SERVICE: $SERVICE
  script:
    - cd services/$SERVICE
    - docker run registry.example.com/$SERVICE:$CI_COMMIT_SHA npm test

deploy:production:
  stage: deploy
  needs:
    - job: test:service
      parallel:
        matrix:
          - SERVICE: [auth, product, order, notification]
  script:
    - ./scripts/deploy-all.sh $CI_COMMIT_SHA
  environment:
    name: production
  when: manual

Bu yapıyla 4 servisin build ve test işleri tamamen paralel çalışıyor. deploy:production ise tüm servislerin testleri geçmesini bekliyor.

DAG Pipeline’da Hata Yönetimi

DAG pipeline’larda hata yönetimi biraz farklı düşünülmeli. Bir job başarısız olduğunda, o job’a needs ile bağlı diğer job’lar otomatik olarak iptal edilir. Bu davranışı yönetmek için birkaç teknik var:

test:critical:
  stage: test
  needs:
    - build:api
  script:
    - pytest tests/critical/ -v
  allow_failure: false  # Bu başarısız olursa pipeline durur

test:non-critical:
  stage: test
  needs:
    - build:api
  script:
    - pytest tests/edge-cases/ -v
  allow_failure: true  # Bu başarısız olsa bile devam eder

# Cleanup job'u her durumda çalışmalı
cleanup:resources:
  stage: .post
  needs: []
  when: always
  script:
    - ./scripts/cleanup-test-resources.sh
    - docker system prune -f

# Notify job'u sadece başarısızlıkta çalışır
notify:failure:
  stage: .post
  needs:
    - job: test:critical
      optional: true
    - job: test:non-critical
      optional: true
  when: on_failure
  script:
    - |
      curl -X POST $SLACK_WEBHOOK 
        -H 'Content-type: application/json' 
        --data "{"text":"Pipeline başarısız: $CI_PROJECT_NAME - $CI_COMMIT_BRANCH"}"

.post stage’i pipeline’ın en sonunda çalışan özel bir stage’dir ve cleanup operasyonları için biçilmiş kaftandır.

Performance Profiling: DAG Kazanımını Ölçmek

DAG’ın size ne kadar zaman kazandırdığını somut olarak görmek için GitLab’ın pipeline analytics özelliğini kullanabilirsiniz. Ama bunu script seviyesinde de ölçmek mümkün:

stages:
  - benchmark

pipeline:timing-report:
  stage: benchmark
  when: always
  script:
    - |
      echo "Pipeline başlangıç zamanı: $CI_PIPELINE_CREATED_AT"
      START=$(date -d "$CI_PIPELINE_CREATED_AT" +%s 2>/dev/null || 
              date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CI_PIPELINE_CREATED_AT" +%s)
      END=$(date +%s)
      DURATION=$((END - START))
      MINUTES=$((DURATION / 60))
      SECONDS=$((DURATION % 60))
      echo "Toplam pipeline süresi: ${MINUTES}dk ${SECONDS}sn"
      
      # Metrikleri artifact olarak kaydet
      cat << EOF > pipeline-metrics.txt
      Pipeline ID: $CI_PIPELINE_ID
      Branch: $CI_COMMIT_BRANCH
      Duration: ${MINUTES}m ${SECONDS}s
      Commit: $CI_COMMIT_SHORT_SHA
      EOF
  artifacts:
    paths:
      - pipeline-metrics.txt
    expire_in: 30 days

Gerçek deneyimlerimden bir örnek vereyim: 12 servisten oluşan bir microservice projesinde klasik stage bazlı pipeline yaklaşık 45 dakika sürüyordu. DAG’a geçtikten sonra bu süre 18 dakikaya indi. Sebep basit: Bağımsız servisler artık birbirini beklemiyor.

Yaygın Hatalar ve Çözümleri

DAG pipeline kurarken en sık yapılan hatalar şunlar:

  • Döngüsel bağımlılık oluşturmak: Job A, Job B’ye muhtaç; Job B de Job A’ya muhtaç. GitLab bunu hata verir ama debug etmesi zaman alabilir. ci lint ile kontrol edin.
  • Çok derin bağımlılık zinciri: A -> B -> C -> D -> E şeklinde 5 kademeli bağımlılık oluşturursanız, DAG’ın faydası azalır. Bağımlılık grafiğini mümkün olduğunca yatay tutun.
  • Artifact boyutlarını göz ardı etmek: Her job’da büyük artifact’lar indiriyorsanız, kazandığınız zamanı network transferine harcarsınız. artifacts: false kullanımını ihmal etmeyin.
  • needs listesini aşırı büyütmek: Bir job için 10-15 bağımlılık tanımladıysanız, büyük ihtimalle tasarımı yeniden düşünmeniz gerekiyor.
# Hatalı: Gereksiz bağımlılıklar
deploy:api:
  stage: deploy
  needs:
    - build:api
    - build:frontend      # Deploy için gerekmez
    - test:api
    - test:frontend       # API deploy için gerekmez
    - security:api
    - lint:api
    - lint:frontend       # API deploy için gerekmez

# Doğru: Sadece gerçekten gerekli bağımlılıklar
deploy:api:
  stage: deploy
  needs:
    - test:api
    - security:api

Sonuç

DAG pipeline, modern yazılım geliştirme süreçlerinde ciddi bir verimlilik silahı. Özellikle microservice mimarileri, monorepo yapıları ve paralel çalışabilecek birden fazla bileşeni olan projelerde farkı ilk pipeline çalıştırmasında hissediyorsunuz.

needs keyword’ü ilk başta basit görünse de olası kullanım kombinasyonları oldukça geniş. optional, artifacts kontrolü, parallel:matrix entegrasyonu ve .pre/.post stage’leriyle birleştiğinde son derece esnek pipeline yapıları kurabilirsiniz.

Pratik önerim: Mevcut bir pipeline’ınızı alın, hangi job’ların birbirinden gerçekten bağımsız olduğunu bir kağıda çizin, bağımlılık grafiğini belirleyin ve needs ekleyerek başlayın. İlk denemede mükemmel olmak zorunda değilsiniz. GitLab’ın pipeline görselleştirme aracı, DAG modunda bağımlılık grafiğini size güzel bir şekilde gösteriyor, oradan hatalarınızı kolayca görüp düzeltebilirsiniz.

CI/CD pipeline’ları sadece “çalışıyor mu” diye bakılacak araçlar değil. Geliştiricilerin günde defalarca beklediği süreçler. Bu bekleme süresini yarıya indirmek, hem bireysel verimliliği hem de takım moralini doğrudan etkiliyor.

Bir yanıt yazın

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