GitLab CI/CD Nedir: Pipeline Temelleri ve Temel Kavramlar
Yazılım geliştirme dünyasında “bende çalışıyor” bahanesi artık kabul görmüyor. Ekipler büyüdükçe, kod tabanları karmaşıklaştıkça ve deployment sıklığı arttıkça manuel süreçler kaçınılmaz olarak kırılıyor. İşte tam bu noktada GitLab CI/CD devreye giriyor ve yazılım teslimat süreçlerini otomatize ederek hem hızı hem de güvenilirliği artırıyor. Bu yazıda GitLab CI/CD’nin temellerini, pipeline kavramını ve gerçek dünya senaryolarıyla nasıl kullanıldığını ele alacağız.
GitLab CI/CD Nedir
GitLab CI/CD, GitLab’ın yerleşik sürekli entegrasyon ve sürekli teslimat sistemidir. Harici bir araç kurmanıza gerek kalmadan, kod deponuzla aynı platformda pipeline’larınızı tanımlayıp çalıştırabilirsiniz. Jenkins gibi alternatiflerin aksine ayrı bir sunucu kurulumu, eklenti yönetimi veya karmaşık entegrasyon konfigürasyonu gerektirmez.
CI (Continuous Integration) yani sürekli entegrasyon, geliştiricilerin kod değişikliklerini sık sık ana dala entegre etmesi ve her entegrasyonun otomatik testlerle doğrulanması pratiğidir. CD (Continuous Delivery/Deployment) ise doğrulanan kodun otomatik olarak staging veya production ortamlarına gönderilmesidir.
GitLab’da tüm bu süreç tek bir YAML dosyasıyla yönetilir: .gitlab-ci.yml
Temel Kavramlar
Pipeline dünyasına girmeden önce terminolojiyi oturtmak gerekiyor. Bu kavramları kafanızda netleştirmeden YAML dosyalarına bakmak kafa karışıklığı yaratır.
Pipeline
Pipeline, bir veya daha fazla stage (aşama) içeren ve belirli bir tetikleyici tarafından başlatılan otomasyon sürecidir. Bir commit attığınızda, merge request açtığınızda veya bir zamanlayıcı tetiklendiğinde pipeline devreye girer.
Stage
Stage’ler pipeline’ın aşamalarıdır. Örneğin build, test, deploy üç farklı stage olabilir. Stage’ler sırayla çalışır ve bir önceki stage başarısız olursa sonraki çalışmaz.
Job
Job, belirli bir stage içinde çalışan görevdir. Bir stage içinde birden fazla job tanımlayabilirsiniz ve bunlar paralel olarak çalışır. Örneğin test stage’inde hem unit test hem de integration test job’u aynı anda çalışabilir.
Runner
Runner, job’ları fiilen çalıştıran ajandır. GitLab.com’un sağladığı shared runner’ları kullanabilir ya da kendi sunucunuza self-hosted runner kurabilirsiniz. Kurumsal ortamlarda genellikle kendi runner’larınızı kurmak daha mantıklıdır.
Artifact
Job çalışırken üretilen ve sonraki job’lara aktarılabilen dosyalardır. Build çıktıları, test raporları, binary dosyalar artifact olarak tanımlanabilir.
İlk .gitlab-ci.yml Dosyası
En basit pipeline tek bir job içerir. Şunu anlayın: kompleksite sonradan gelir, önce temeli kurmak lazım.
# .gitlab-ci.yml - En basit hali
stages:
- test
run_tests:
stage: test
image: python:3.11
script:
- pip install -r requirements.txt
- python -m pytest tests/
Bu dosyayı projenizin kök dizinine koyup commit attığınızda GitLab otomatik olarak pipeline’ı başlatır. image direktifi Docker imajını belirtir, script ise çalıştırılacak komutları.
Gerçek Bir Pipeline Yapısı
Tek job’lu pipeline’lar pratikte işe yaramaz. Gerçek dünya senaryosunda bir Node.js uygulaması için şu yapıyı düşünelim:
# .gitlab-ci.yml - Node.js uygulaması için kapsamlı pipeline
stages:
- install
- lint
- test
- build
- deploy
variables:
NODE_ENV: "production"
DOCKER_IMAGE: "registry.gitlab.com/sirketim/uygulama"
# Bağımlılıkları cache'le, her seferinde npm install çekme
cache:
paths:
- node_modules/
install_dependencies:
stage: install
image: node:18-alpine
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
lint_check:
stage: lint
image: node:18-alpine
script:
- npm run lint
dependencies:
- install_dependencies
unit_tests:
stage: test
image: node:18-alpine
script:
- npm run test:unit -- --coverage
coverage: '/Liness*:s*(d+.?d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
dependencies:
- install_dependencies
build_app:
stage: build
image: node:18-alpine
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 day
only:
- main
- develop
Bu yapıda dikkat etmeniz gereken birkaç nokta var:
- cache direktifi
node_modulesklasörünü saklar, böylece her pipeline çalışmasındanpm installyapmak zorunda kalmazsınız - artifacts ile
dist/klasörünü sonraki stage’lere aktarıyoruz - only ile hangi branch’lerde çalışacağını belirliyoruz
- dependencies ile hangi önceki job’un artifact’larına ihtiyaç duyulduğunu belirtiyoruz
Environment Variables ve Secrets Yönetimi
Pipeline’larda en kritik konulardan biri hassas bilgilerin güvenli saklanmasıdır. Database şifreleri, API anahtarları, SSH private key’leri asla .gitlab-ci.yml dosyasına yazılmaz.
GitLab’da bu bilgileri Settings > CI/CD > Variables bölümünde saklarsınız. Sonra pipeline içinde direkt kullanabilirsiniz:
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d 'r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -H $PRODUCTION_SERVER >> ~/.ssh/known_hosts
script:
- ssh deploy@$PRODUCTION_SERVER "cd /var/www/app && git pull && npm install --production && pm2 restart app"
environment:
name: production
url: https://uygulamam.com
only:
- main
Burada $SSH_PRIVATE_KEY ve $PRODUCTION_SERVER GitLab Variables’dan geliyor. Protected olarak işaretlenmiş variable’lar sadece protected branch’lerde çalışan job’larda görünür, bu da güvenlik açısından kritik.
Variable türleri şöyle ayrılır:
- Variable: Normal metin değeri, log’larda maskeleme yapılabilir
- File: İçerik geçici bir dosyaya yazılır, dosyanın path’i variable olarak atanır (SSL sertifikaları için ideal)
- Masked: Log çıktısında görünmez ama her formatta maskelenemiyor dikkat
- Protected: Sadece protected branch/tag’lerde kullanılabilir
Docker ile Pipeline Çalıştırma
Modern CI/CD neredeyse Docker olmadan düşünülemez. Her job farklı bir Docker imajında çalışabilir, bu da bağımlılık çakışmalarını ortadan kaldırır.
# Python ve PostgreSQL gerektiren bir test ortamı
test_integration:
stage: test
image: python:3.11-slim
services:
- postgres:14-alpine
- redis:7-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
DATABASE_URL: "postgresql://testuser:testpass@postgres/testdb"
REDIS_URL: "redis://redis:6379"
before_script:
- pip install -r requirements.txt
- python manage.py migrate
script:
- python -m pytest tests/integration/ -v
services direktifi ilginç bir konsept. Ana container yanında sidecar olarak başka container’lar çalıştırmanızı sağlar. PostgreSQL, Redis, MySQL, Elasticsearch gibi bağımlılıkları bu şekilde test ortamınıza dahil edebilirsiniz. Service container’lar servis adıyla (örneğin postgres, redis) erişilebilir olur.
Koşullu Çalışma: rules ve only/except
Her job’un her durumda çalışmasını istemezsiniz. Production deploy’u sadece main branch’ten yapmalısınız, test coverage raporunu sadece merge request’lerde görmek isteyebilirsiniz.
only/except eski yöntem, rules ise modern ve çok daha esnek yaklaşım:
# rules ile gelişmiş koşul yönetimi
deploy_staging:
stage: deploy
script:
- ./deploy.sh staging
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
when: on_success
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- when: never
deploy_production:
stage: deploy
script:
- ./deploy.sh production
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
when: manual
environment:
name: production
notify_slack:
stage: .post
script:
- 'curl -X POST -H "Content-type: application/json" --data "{"text":"Deploy tamamlandi: $CI_PROJECT_NAME"}" $SLACK_WEBHOOK_URL'
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: on_success
when: manual özellikle dikkat edin. Bu, job’un otomatik çalışmayacağını, birinin GitLab arayüzünden manuel olarak tetiklemesi gerektiğini belirtir. Production deploy’lar için bu yaklaşım çok yaygın ve yerinde.
Sık kullanılan predefined variable’lar:
- CI_COMMIT_BRANCH: Commit’in yapıldığı branch adı
- CI_COMMIT_SHA: Commit hash değeri
- CI_PIPELINE_SOURCE: Pipeline’ı tetikleyen kaynak (push, merge_request_event, schedule vb.)
- CI_PROJECT_NAME: Proje adı
- CI_REGISTRY_IMAGE: Proje’nin GitLab Container Registry adresi
- CI_COMMIT_TAG: Eğer bir tag üzerindeyse tag adı
Docker Image Build ve Registry’ye Push
GitLab’ın kendi container registry’si var ve CI/CD ile mükemmel entegre çalışıyor:
# Docker imajı build edip GitLab Registry'ye push etme
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
build_docker:
stage: build
image: docker:24
services:
- docker:24-dind
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 GIT_COMMIT=$CI_COMMIT_SHA
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-t $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- tags
$CI_REGISTRY, $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD otomatik olarak GitLab tarafından sağlanan variable’lar. Ekstra konfigürasyon gerektirmez.
Include ile Pipeline’ları Modüler Tutmak
Birden fazla proje yönetiyorsanız aynı pipeline kodunu tekrar tekrar yazmak yerine merkezi template’ler kullanmak hayat kurtarır.
# Ana .gitlab-ci.yml - Diğer template'leri include eder
include:
- project: 'devops/ci-templates'
ref: main
file: '/templates/docker-build.yml'
- project: 'devops/ci-templates'
ref: main
file: '/templates/security-scan.yml'
- local: '.gitlab/ci/deploy.yml'
stages:
- test
- build
- security
- deploy
# Lokalde tanımlı özel job
custom_test:
stage: test
script:
- echo "Projeye özel test"
Bu yaklaşımın güzelliği şu: Merkezi template’de bir güvenlik tarama aracını güncellerseniz tüm projeler otomatik olarak güncellenen versiyonu kullanır. 50 proje yönetirken bu özellik inanılmaz değerli.
Self-Hosted Runner Kurulumu
GitLab.com’un shared runner’larını kullanmak başlangıç için iyi, ama production ortamında kendi runner’larınızı kurmak çok daha mantıklı. Hem maliyet hem de güvenlik açısından.
# Ubuntu/Debian üzerine GitLab Runner kurulumu
# Repository ekle
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
# Runner'ı kur
sudo apt-get install gitlab-runner
# Runner'ı kaydet (GitLab > Settings > CI/CD > Runners'dan token al)
sudo gitlab-runner register
--url "https://gitlab.com/"
--registration-token "BURAYA_TOKEN_GEL"
--executor "docker"
--docker-image "alpine:latest"
--description "production-runner-01"
--tag-list "production,docker"
--run-untagged="false"
--locked="false"
# Servisi başlat ve enable et
sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runner
# Runner durumunu kontrol et
sudo gitlab-runner status
Runner kayıt işleminde executor seçimi önemli. docker executor her job’u temiz bir container içinde çalıştırır, bu da en güvenli ve tavsiye edilen yöntem. shell executor doğrudan runner’ın shell’inde çalışır, daha hızlı ama daha riskli.
Runner’a tag atamak kritik: Job tanımında tags: [production] yazdığınızda o job sadece bu tag’e sahip runner’larda çalışır. Bu sayede production deploy job’larınızı sadece production ağında bulunan runner’larda çalıştırabilirsiniz.
Pipeline Hata Ayıklama
Pipeline’lar ilk seferinde nadiren düzgün çalışır. Hata ayıklamak için birkaç pratik yöntem:
# Pipeline içinde debug bilgisi toplama
debug_job:
stage: test
script:
# Ortam değişkenlerini listele (hassas bilgileri maskele!)
- env | grep -v PASSWORD | grep -v SECRET | sort
# Hangi kullanıcı olduğunu gör
- whoami
# Çalışma dizini
- pwd && ls -la
# Network erişimini test et
- curl -f https://api.sirket.com/health || echo "API erişilemiyor"
# Docker varsa kontrol et
- docker info 2>/dev/null || echo "Docker mevcut degil"
when: manual
Job başarısız olduğunda GitLab’da log’ları incelemek ilk adım. Eğer log’lardan anlayamıyorsanız, yukarıdaki gibi debug job’u ekleyip environment’ı inceleyebilirsiniz.
CI_DEBUG_TRACE variable’ını true olarak ayarlarsanız shell trace aktif olur ve her komutun çıktısını görürsünüz. Ama dikkat: Bu modda variable değerleri de görünebilir, production’da asla kullanmayın.
Gerçek Senaryo: Microservice Deployment Pipeline’ı
Eksiksiz bir senaryo görelim. Bir e-ticaret uygulamasının ödeme servisini Kubernetes’e deploy eden pipeline:
# Ödeme servisi için tam pipeline
stages:
- validate
- test
- build
- scan
- deploy-staging
- test-staging
- deploy-production
variables:
SERVICE_NAME: "payment-service"
K8S_NAMESPACE_STAGING: "ecommerce-staging"
K8S_NAMESPACE_PROD: "ecommerce-production"
# Staging deploy
deploy_to_staging:
stage: deploy-staging
image: bitnami/kubectl:latest
before_script:
- echo $KUBECONFIG_STAGING | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- kubectl set image deployment/$SERVICE_NAME
$SERVICE_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-n $K8S_NAMESPACE_STAGING
- kubectl rollout status deployment/$SERVICE_NAME
-n $K8S_NAMESPACE_STAGING
--timeout=5m
environment:
name: staging
url: https://staging.odeme.sirket.com
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
# Staging smoke test
smoke_test_staging:
stage: test-staging
image: alpine/curl:latest
script:
- sleep 10
- curl -f https://staging.odeme.sirket.com/health
- curl -f https://staging.odeme.sirket.com/api/v1/status
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
# Production deploy - manuel onay gerektirir
deploy_to_production:
stage: deploy-production
image: bitnami/kubectl:latest
before_script:
- echo $KUBECONFIG_PRODUCTION | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- kubectl set image deployment/$SERVICE_NAME
$SERVICE_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-n $K8S_NAMESPACE_PROD
- kubectl rollout status deployment/$SERVICE_NAME
-n $K8S_NAMESPACE_PROD
--timeout=10m
environment:
name: production
url: https://odeme.sirket.com
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
allow_failure: false
Bu pipeline’da staging’e otomatik, production’a manuel onaylı deploy yapılıyor. Staging’e deploy sonrasında smoke test çalışıyor ve başarısız olursa production deploy’u zaten mevcut olmayan bir önceki stage başarısız olduğu için çalışmayacak.
Pipeline Performansını Artırma
Uzun süren pipeline’lar geliştirici verimliliğini düşürür. Bazı pratik optimizasyonlar:
- Cache kullanımı: npm, pip, maven gibi paket yöneticisi cache’lerini saklayın. 5 dakikalık install süresi 30 saniyeye düşebilir.
- Paralel job’lar: Bağımsız testleri farklı job’lara bölün, aynı anda çalışsınlar.
- needs direktifi: Varsayılan davranış stage’lerin sırayla çalışmasıdır.
needsile stage sırasını kısmen atlayıp bir job bitince başka stage’deki job’u başlatabilirsiniz. - İnce Docker imajları:
node:18yerinenode:18-alpinekullanmak pull süresini ciddi düşürür. - Shallow clone: Büyük repolarda
GIT_DEPTH: 1ile sadece son commit’i çekerek clone süresini kısaltın.
# Performans optimizasyonu örnekleri
variables:
GIT_DEPTH: "1" # Shallow clone
fast_test:
stage: test
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .cache/pip
- venv/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
script:
- python -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt
- pytest -x -q
needs: [] # Önceki stage bitmeden çalışmaya başla
Sonuç
GitLab CI/CD, yazılım teslimat süreçlerini standartlaştırmak ve otomatize etmek için güçlü bir platform sunuyor. Tek bir YAML dosyasıyla test, build, güvenlik taraması ve deployment’ı yönetebiliyorsunuz.
Başlarken küçük tutun: Tek stage, birkaç job, temel testler. Pipeline çalıştıktan sonra kademeli olarak ekleyin. Docker image build, registry entegrasyonu, multi-environment deployment gibi özellikler zaman içinde eklenir.
En çok dikkat etmeniz gereken konular şunlar: Variable ve secret güvenliği asla ihmal etmeyin, her job için uygun Docker imajı seçin, artifact’ları gerektiğinden uzun süre saklamayın (depolama maliyeti artar), ve runner’larınızı düzenli güncelleyin.
Pratik açıdan bakarsanız GitLab CI/CD’yi gerçekten öğrenmenin tek yolu bir proje üzerinde uygulamak. Mevcut bir projeniz varsa .gitlab-ci.yml ekleyerek başlayın, ilk pipeline çalıştığında geri kalan her şey daha mantıklı oturmaya başlıyor.
