.gitlab-ci.yml Dosyası Yazımı: Temel Yapı ve Kullanım Kılavuzu

GitLab’a ilk pipeline’ını kurmaya çalışırken saatlerce dokümantasyon okuduğunu hatırlıyor musun? Ben de hatırlıyorum. .gitlab-ci.yml dosyası ilk bakışta oldukça sade görünür ama içine girince “bu ne biçim yapı” diyebiliyorsun. Bu yazıda sıfırdan başlayarak gerçek dünya senaryolarıyla .gitlab-ci.yml dosyasının temel yapısını ele alacağız.

.gitlab-ci.yml Nedir ve Nereye Koyulur?

GitLab CI/CD sistemi, projenin kök dizininde bulunan .gitlab-ci.yml adındaki YAML dosyasını okuyarak pipeline’ı çalıştırır. Her push işleminde, merge request açıldığında veya manuel tetikleme yapıldığında GitLab bu dosyayı parse eder ve tanımlı işleri (job) sırayla ya da paralel olarak çalıştırır.

Dosyanın yeri kesindir: projenin kök dizini. src/ altına ya da başka bir klasöre koyamazsın, GitLab sadece kök dizine bakar. Dosyayı oluştururken dikkat etmen gereken en önemli nokta YAML syntax’ıdır. Tek bir yanlış girinti tüm pipeline’ı patlatır.

Başlamadan önce şunu söyleyeyim: .gitlab-ci.yml sadece bir konfigürasyon dosyası değil, aynı zamanda bir belgedir. İyi yazılmış bir CI dosyası, yeni gelen bir ekip üyesine projenin nasıl build edildiğini, test edildiğini ve deploy edildiğini anlatır.

Temel Yapı: Stage ve Job Kavramları

Pipeline’ın iskeletini iki kavram oluşturur: stage (aşama) ve job (iş). Stage’ler pipeline’ın genel akışını tanımlar, job’lar ise bu aşamalarda çalışacak gerçek komutları barındırır.

stages:
  - build
  - test
  - deploy

build-app:
  stage: build
  script:
    - echo "Uygulama derleniyor..."
    - npm install
    - npm run build

run-tests:
  stage: test
  script:
    - echo "Testler çalıştırılıyor..."
    - npm run test

deploy-production:
  stage: deploy
  script:
    - echo "Production'a deploy ediliyor..."
    - ./deploy.sh

Bu örnekte üç stage tanımlandı. GitLab önce build stage’indeki tüm job’ları çalıştırır, başarılı olursa test stage’ine geçer, orası da geçerse deploy çalışır. Herhangi bir stage başarısız olursa pipeline durur ve sonraki stage’ler çalışmaz.

Önemli nokta: Bir stage içinde birden fazla job varsa, bunlar paralel olarak çalışır. Farklı stage’lerdeki job’lar ise sıralı çalışır.

image: Anahtar Kelimesi ile Docker Image Seçimi

GitLab Runner job’ları çalıştırmak için Docker image kullanır. image anahtar kelimesiyle hangi container’ın kullanılacağını belirtirsin.

image: node:18-alpine

stages:
  - build
  - test

build-app:
  stage: build
  script:
    - node --version
    - npm ci
    - npm run build

unit-tests:
  stage: test
  script:
    - npm run test:unit

integration-tests:
  stage: test
  image: node:20-alpine
  script:
    - npm run test:integration

Burada global olarak node:18-alpine tanımlandı ama integration-tests job’u kendi image değerini override etti ve node:20 kullandı. Bu esneklik gerçek projelerde çok işe yarar. Mesela test ortamında farklı bir PHP versiyonu deniyorsun ama build ortamını değiştirmek istemiyorsun, tam bu senaryo için biçilmiş kaftan.

variables: ile Değişken Tanımlama

Pipeline içinde tekrar eden değerleri ya da gizli tutulması gereken bilgileri variables bloğuyla yönetirsin.

variables:
  APP_ENV: "production"
  NODE_VERSION: "18"
  DOCKER_REGISTRY: "registry.example.com"
  BUILD_DIR: "./dist"

stages:
  - build
  - test
  - deploy

build-frontend:
  stage: build
  image: node:${NODE_VERSION}-alpine
  variables:
    NODE_OPTIONS: "--max-old-space-size=4096"
  script:
    - echo "Ortam: $APP_ENV"
    - npm ci
    - npm run build -- --output-path=$BUILD_DIR

Global variables pipeline genelinde geçerliyken, job seviyesinde variables sadece o job için geçerlidir ve global değerleri override eder.

Hassas veriler için (database şifreleri, API anahtarları) doğrudan .gitlab-ci.yml içine değil, GitLab’ın Settings > CI/CD > Variables bölümüne ekle. Bu değerleri $MY_SECRET şeklinde kullanabilirsin ve log’larda maskelenir.

before_script ve after_script Kullanımı

Her job’dan önce veya sonra çalışmasını istediğin komutlar için bu bloklar kullanılır. Tekrar eden setup işlemlerini buraya almak kodu temizler.

image: python:3.11-slim

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"

before_script:
  - echo "Pipeline başlıyor: $CI_PIPELINE_ID"
  - pip install --upgrade pip
  - pip install -r requirements.txt

stages:
  - test
  - lint
  - deploy

run-pytest:
  stage: test
  script:
    - pytest tests/ -v --tb=short
  after_script:
    - echo "Test tamamlandı, rapor oluşturuluyor..."
    - pytest tests/ --html=report.html --self-contained-html
  artifacts:
    paths:
      - report.html
    expire_in: 1 week

run-flake8:
  stage: lint
  before_script:
    - pip install flake8
  script:
    - flake8 src/ --max-line-length=100

Dikkat et: Job seviyesinde before_script tanımlarsan, global before_script tamamen devre dışı kalır, birleşmez. run-flake8 job’unda global before_script çalışmaz, sadece job’un kendi before_script‘i çalışır. Bu davranış başta kafa karıştırabilir.

artifacts: ile Dosya Paylaşımı

Job’lar birbirinden izole container’larda çalışır. Bir job’un ürettiği dosyayı bir sonraki job’da kullanmak istiyorsan artifacts kullanman gerekir.

stages:
  - build
  - test
  - package

build-backend:
  stage: build
  image: maven:3.9-openjdk-17
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 2 hours

run-integration-test:
  stage: test
  image: maven:3.9-openjdk-17
  dependencies:
    - build-backend
  script:
    - ls -la target/
    - java -jar target/app.jar &
    - sleep 10
    - mvn test -Pintegration-tests
  artifacts:
    reports:
      junit: target/surefire-reports/*.xml
    when: always

create-docker-image:
  stage: package
  image: docker:24
  dependencies:
    - build-backend
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .
    - docker push myapp:$CI_COMMIT_SHA

expire_in önemli bir parametre. Artifact’leri sonsuza kadar saklamak GitLab storage’ini şişirir. Makul bir süre ver: build artifact’leri için 2 hours yeterli, test raporları için 1 week mantıklı.

when: always ise job başarısız olsa bile artifact’lerin saklanmasını sağlar. Test raporlarına bu şekilde davranmak iyi bir pratiktir, çünkü başarısız testlerin raporunu da görmek istersin.

only, except ve rules: Pipeline Tetikleme Koşulları

Her job’un her push’ta çalışması gerekmez. rules anahtar kelimesiyle (eski yöntem only/except) ince ayar yapabilirsin.

stages:
  - test
  - build
  - deploy

run-tests:
  stage: test
  script:
    - npm test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH == "develop"'

build-staging:
  stage: build
  script:
    - npm run build:staging
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
      when: on_success

build-production:
  stage: build
  script:
    - npm run build:production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: on_success

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
      when: on_success

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://myapp.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

when: manual çok kullanışlı. Production deploy’u otomatik yapmanı istemiyorsan, pipeline’da bir “onay butonu” oluşturur. GitLab UI’dan bu butona basılmadan deploy çalışmaz.

only/except kullanımı eski stil ve GitLab tarafından deprecated sayılıyor. Yeni yazacağın pipeline’larda rules kullan.

cache: ile Bağımlılıkları Hızlandırma

npm install ya da pip install her seferinde çalışmak zorunda değil. cache ile bağımlılık klasörlerini runner’lar arasında paylaşabilirsin.

image: node:18-alpine

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

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

stages:
  - install
  - test
  - build

install-deps:
  stage: install
  script:
    - npm ci --cache .npm --prefer-offline

run-unit-tests:
  stage: test
  script:
    - npm run test:unit -- --coverage
  coverage: '/Liness*:s*(d+.?d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build-app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - build/
    expire_in: 1 day

Cache key’ini package-lock.json dosyasına bağladım. Bu sayede dosya değişmediği sürece aynı cache kullanılır, değişirse cache temizlenir ve yeniden oluşturulur. Çok akıllıca bir yaklaşım.

coverage satırındaki regex pattern, test çıktısından coverage yüzdesini çeker ve GitLab UI’da gösterir. Her test framework’ünün farklı bir pattern’i vardır, bunu projeye göre ayarla.

needs: ile Paralel Pipeline Optimizasyonu

Varsayılan olarak bir stage’deki tüm job’lar bir önceki stage tamamlanmadan başlamaz. needs anahtar kelimesiyle bu bağımlılığı daha ince tanımlayabilir, bazı job’ları beklemeden başlatabilirsin.

stages:
  - build
  - test
  - security
  - deploy

build-frontend:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

build-backend:
  stage: build
  script:
    - go build -o bin/server ./cmd/server
  artifacts:
    paths:
      - bin/

test-frontend:
  stage: test
  needs:
    - build-frontend
  script:
    - npm test

test-backend:
  stage: test
  needs:
    - build-backend
  script:
    - go test ./...

sast-scan:
  stage: security
  needs: []
  script:
    - ./run-sast.sh
  allow_failure: true

dependency-check:
  stage: security
  needs: []
  script:
    - ./run-dependency-check.sh
  allow_failure: true

deploy-app:
  stage: deploy
  needs:
    - test-frontend
    - test-backend
  script:
    - kubectl apply -f k8s/

needs: [] ile sast-scan job’u hiçbir job’u beklemeden, pipeline başlar başlamaz çalışmaya başlar. Bu sayede build tamamlanmayı beklemek zorunda değildir.

allow_failure: true ile job başarısız olsa bile pipeline devam eder. Güvenlik taramaları için bu mantıklı olabilir, ama dikkatli kullan. Kritik job’lara asla allow_failure: true ekleme.

include: ile Yapıyı Modülerleştirme

Pipeline’ın büyüdükçe tek .gitlab-ci.yml dosyası yönetilemez hale gelir. include ile farklı dosyalara bölebilirsin.

include:
  - local: '.gitlab/ci/build.yml'
  - local: '.gitlab/ci/test.yml'
  - local: '.gitlab/ci/deploy.yml'
  - project: 'devops-team/ci-templates'
    ref: main
    file: '/templates/security-scan.yml'
  - template: 'Security/SAST.gitlab-ci.yml'

variables:
  ENVIRONMENT: "production"
  DOCKER_REGISTRY: "registry.gitlab.com/myorg"

stages:
  - build
  - test
  - security
  - deploy

local ile aynı repo içindeki dosyaları dahil edersin. project ile başka bir GitLab projesinden template çekersin, bu özellikle büyük organizasyonlarda CI/CD standardizasyonu için harika çalışır. template ise GitLab’ın kendi built-in template’lerini kullanır.

.gitlab/ci/build.yml dosyası şöyle görünebilir:

build-docker:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA .
    - docker build -t $DOCKER_REGISTRY/$CI_PROJECT_NAME:latest .
    - docker push $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
    - docker push $DOCKER_REGISTRY/$CI_PROJECT_NAME:latest
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Gerçek Dünya Senaryosu: Tam Bir Node.js Pipeline

Şimdiye kadar öğrendiklerini bir araya getirelim. Bir Node.js web uygulaması için production-ready bir pipeline:

image: node:18-alpine

variables:
  NODE_ENV: "test"
  APP_NAME: "my-web-app"
  DOCKER_REGISTRY: "registry.gitlab.com/myorg"

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

before_script:
  - npm ci --cache .npm --prefer-offline

stages:
  - validate
  - test
  - build
  - deploy

lint-code:
  stage: validate
  script:
    - npm run lint
    - npm run format:check

type-check:
  stage: validate
  script:
    - npm run type-check

unit-tests:
  stage: test
  needs:
    - lint-code
    - type-check
  script:
    - npm run test:unit -- --coverage
  coverage: '/All files[^|]*|[^|]*s+([d.]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
      junit:
        - junit.xml
    when: always
    expire_in: 1 week

e2e-tests:
  stage: test
  image: cypress/browsers:node18-chrome105
  needs:
    - lint-code
  before_script:
    - npm ci
  script:
    - npm run test:e2e
  artifacts:
    paths:
      - cypress/screenshots/
      - cypress/videos/
    when: on_failure
    expire_in: 3 days
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

build-production:
  stage: build
  needs:
    - unit-tests
  variables:
    NODE_ENV: "production"
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 4 hours
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-production:
  stage: deploy
  image: alpine:latest
  needs:
    - build-production
    - e2e-tests
  before_script:
    - apk add --no-cache curl
  script:
    - curl -X POST "$DEPLOY_WEBHOOK_URL" -H "Authorization: Bearer $DEPLOY_TOKEN"
  environment:
    name: production
    url: https://myapp.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  allow_failure: false

Bu pipeline şunları yapıyor:

  • validate stage’inde lint ve type check paralel çalışır
  • test stage’inde unit test ve e2e testler çalışır, ama validate tamamlanmadan başlamaz
  • build sadece main branch’te çalışır ve testlerin geçmesini bekler
  • deploy manuel onay gerektirir, üretim ortamına push butona basılmadan gitmez

Sık Yapılan Hatalar

.gitlab-ci.yml yazarken en çok karşılaşılan sorunları şöyle sıralayayım:

  • Girinti hatası: YAML’da tab kullanma, her zaman space kullan. 2 veya 4 space, tutarlı ol.
  • Stage tanımlamayı unutmak: Job’da kullandığın stage değeri, üstteki stages listesinde tanımlı olmalı.
  • Cache ve artifact karıştırmak: Cache hız için, artifact job’lar arası veri transferi için kullanılır.
  • Global before_script override davranışını atlama: Job’da before_script tanımlarsan global olan çalışmaz.
  • Her şeyi tek dosyaya sıkıştırma: Pipeline büyüyünce include kullanmaktan çekinme.
  • needs ve stage sıralamasını karıştırma: needs ile belirttiğin job’lar, bulunduğu stage’den önce veya aynı stage’de olabilir, ama sonraki stage’den olamaz.

Sonuç

.gitlab-ci.yml dosyası öğrenmesi birkaç günü bulan ama bir kez öğrenince çok güçlü bir araç. Temel yapıyı kavramak için stages, script, artifacts ve rules dörtlüsünü sindirmen yeterli. Geri kalanı ihtiyaç doğdukça öğrenebilirsin.

Pratik önerim: Yeni bir projeye başlarken aşırı karmaşık bir pipeline yazmaya çalışma. Önce sadece test çalıştıran minimal bir .gitlab-ci.yml ile başla, sonra aşama aşama build ve deploy adımlarını ekle. Düzgün çalışmayan bir megalomanik pipeline’dan, basit ama güvenilir çalışan bir pipeline çok daha değerlidir.

GitLab’ın kendi linter aracını da unutma: Projenin CI/CD > Editor bölümünde .gitlab-ci.yml dosyasını yapıştırıp syntax kontrolü yapabilirsin. Her push’tan önce bunu kullanmak bayağı zaman kurtarır.

Bir yanıt yazın

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