Ana içeriğe geç

GitLab CI Recipes — DAG, Dynamic Child, Multi-Project#

"GitLab CI 'GitHub Actions klonu' değil — DAG-native + monorepo friendly. Doğru kullanılırsa 50 servisli monorepo'yu dakikalarda işler."

Bu rehber GitLab CI/CD'nin pratik tariflerini — DAG pipeline, dynamic child, multi-project trigger, OIDC AWS auth — anlatır.


📐 GitLab CI Anatomi#

# .gitlab-ci.yml
stages:
  - build
  - test
  - scan
  - deploy

variables:
  DOCKER_BUILDKIT: 1

default:
  image: <REGISTRY>/<CI_BASE>:latest
  before_script:
    - echo "starting"

# Job tanımı
build-app:
  stage: build
  script:
    - make build
  artifacts:
    paths: [dist/]
    expire_in: 1 week

🌐 Recipe 1: DAG Pipeline (needs)#

DAG: Stage'leri sıralı değil, dependency graph ile çalıştır.

build-backend:
  stage: build
  script: make backend

build-frontend:
  stage: build
  script: make frontend

test-backend:
  stage: test
  needs: [build-backend]    # frontend bekleme
  script: make test-backend

test-frontend:
  stage: test
  needs: [build-frontend]
  script: make test-frontend

scan-backend:
  stage: scan
  needs: [build-backend]
  script: trivy image <BACKEND>

deploy:
  stage: deploy
  needs: [test-backend, test-frontend, scan-backend]
  script: make deploy

→ Backend'i bekleyenler frontend'in bitmesini beklemiyor; paralel.


🧬 Recipe 2: Dynamic Child Pipeline#

Senaryo: Monorepo'da 50 servis. Sadece değişen servisin pipeline'ını çalıştır.

# .gitlab-ci.yml (parent)
generate-pipeline:
  stage: build
  image: alpine:latest
  script:
    - apk add --no-cache git python3
    # Detect changed services
    - python3 scripts/generate-child.py > generated.yml
  artifacts:
    paths: [generated.yml]

trigger-children:
  stage: test
  needs: [generate-pipeline]
  trigger:
    include:
      - artifact: generated.yml
        job: generate-pipeline
    strategy: depend
# scripts/generate-child.py
import os, yaml
from subprocess import check_output

# Son commit'te değişen path'ler
changed = check_output(["git", "diff", "--name-only", "HEAD~1"]).decode().splitlines()

services = set()
for f in changed:
    if f.startswith("services/"):
        services.add(f.split("/")[1])

config = {"stages": ["test"]}
for svc in services:
    config[f"test-{svc}"] = {
        "stage": "test",
        "script": f"cd services/{svc} && make test"
    }

print(yaml.dump(config))

🔑 50 servis × 5 dk = 250 dk. Dynamic child + selective testing → 5-10 dk.


🔁 Recipe 3: Multi-Project Pipeline Trigger#

# Repo A (orchestrator)
deploy-downstream:
  stage: deploy
  trigger:
    project: <ORG>/<DOWNSTREAM_REPO>
    branch: main
    strategy: depend     # downstream sonucunu bekle
  variables:
    UPSTREAM_VERSION: $CI_COMMIT_TAG
# Repo B (downstream)
deploy:
  rules:
    - if: $CI_PIPELINE_SOURCE == "pipeline"
  script:
    - echo "Triggered by upstream version $UPSTREAM_VERSION"
    - ./deploy.sh

🔐 Recipe 4: OIDC ile AWS#

deploy:
  id_tokens:
    AWS_TOKEN:
      aud: https://gitlab.example.com
  variables:
    AWS_ROLE_ARN: arn:aws:iam::<ACCOUNT>:role/gitlab-deploy
  script:
    - >
      export AWS_CREDS=$(aws sts assume-role-with-web-identity
        --role-arn $AWS_ROLE_ARN
        --role-session-name gitlab-${CI_JOB_ID}
        --web-identity-token $AWS_TOKEN
        --duration-seconds 3600)
    - export AWS_ACCESS_KEY_ID=$(echo $AWS_CREDS | jq -r .Credentials.AccessKeyId)
    - export AWS_SECRET_ACCESS_KEY=$(echo $AWS_CREDS | jq -r .Credentials.SecretAccessKey)
    - export AWS_SESSION_TOKEN=$(echo $AWS_CREDS | jq -r .Credentials.SessionToken)
    - aws s3 ls

🚀 Recipe 5: Docker Build + Sign + Push (Kaniko)#

build:
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - >
      /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile Dockerfile
      --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      --cache=true
      --cache-repo=$CI_REGISTRY_IMAGE/cache

sign:
  image: gcr.io/projectsigstore/cosign:latest
  needs: [build]
  variables:
    COSIGN_EXPERIMENTAL: "true"
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - cosign sign --yes $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

⚠️ Docker-in-Docker (DinD) kullanma — security risk. Kaniko veya Buildah tercih.


💾 Recipe 6: Cache Strategies#

default:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
      - .npm/

# Veya per-branch
cache:
  key: $CI_COMMIT_REF_SLUG
  paths: [.cache/]

# Dotenv artifact (env var pass between jobs)
build:
  script:
    - echo "BUILD_VERSION=$(git rev-parse --short HEAD)" > build.env
  artifacts:
    reports:
      dotenv: build.env

deploy:
  needs: [build]
  script:
    - echo "Deploying $BUILD_VERSION"   # auto-loaded

🌍 Recipe 7: Environment + Manual Gate#

deploy-staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.<DOMAIN>
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script: ./deploy-staging.sh

deploy-prod:
  stage: deploy
  environment:
    name: production
    url: https://prod.<DOMAIN>
    deployment_tier: production
  rules:
    - if: $CI_COMMIT_TAG
      when: manual    # manuel onay
      allow_failure: false
  needs: [deploy-staging]
  script: ./deploy-prod.sh

UI: Operate → Environments → Production → Required approvals.


🔄 Recipe 8: Rules + Conditional Workflows#

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG

test:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "src/**"
    - if: $CI_COMMIT_BRANCH == "main"
  script: make test

🧪 Recipe 9: Test Reports#

test:
  script:
    - npm test -- --reporter junit --output-file junit.xml
  artifacts:
    when: always
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

→ MR'da test sonucu, coverage diff otomatik gösterilir.


🛡️ Recipe 10: SAST + DAST + Container Scan (Built-in)#

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/DAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

variables:
  DAST_WEBSITE: https://staging.<DOMAIN>

→ Built-in scanner'lar, MR widget'ında bulgular görünür.


🚫 Anti-Pattern Tablosu#

Anti-pattern Niye kötü Doğru
Stage'leri sıralı (needs yok) Paralelleşmiyor DAG ile needs
50 servis tek pipeline Yavaş Dynamic child
image: docker:latest (DinD) Privileged, security risk Kaniko / Buildah
Long-lived AWS key Compromise OIDC
Manuel deploy gate yok prod'da Direct deploy bug Manual + protected env
Cache key static İnvalidasyon yok files: hash
Workflow rules yok Her event pipeline tetikler workflow.rules
Built-in security scan kapalı CVE birikiyor Include security templates
Artifact uzun retention Storage maliyet expire_in: 1 week
Self-hosted runner persistent Side-channel Ephemeral runner
Variable scope yanlış Secret leak Protected variable + masked

📋 GitLab CI Production Checklist#

[ ] DAG ile `needs` (paralel pipeline)
[ ] Cache: per-branch + lockfile hash
[ ] OIDC (id_tokens) cloud auth
[ ] Kaniko / Buildah (DinD yerine)
[ ] cosign sign + Sigstore OIDC
[ ] Dynamic child (monorepo'da)
[ ] Workflow rules (gereksiz pipeline yok)
[ ] Path filter (`changes:`)
[ ] Environment protection: prod manual
[ ] Built-in security templates
[ ] Test reports (junit, coverage)
[ ] Slack notification on failure
[ ] Artifact expire_in
[ ] Protected variables (prod secrets)
[ ] Self-hosted runner ephemeral

📚 Referanslar#


"GitLab CI'ı 'YAML jungle' yapan stage sıralı kafayla kullanmaktır. DAG + dynamic child ile aynı YAML, 3x daha hızlı monorepo işler."