.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:
validatestage’inde lint ve type check paralel çalışırteststage’inde unit test ve e2e testler çalışır, ama validate tamamlanmadan başlamazbuildsadece main branch’te çalışır ve testlerin geçmesini beklerdeploymanuel 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
stageslistesinde 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_scripttanımlarsan global olan çalışmaz. - Her şeyi tek dosyaya sıkıştırma: Pipeline büyüyünce
includekullanmaktan çekinme. - needs ve stage sıralamasını karıştırma:
needsile 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.
