GitLab CI/CD ile Test ve Build Pipeline Kurulumu

Yazılım geliştirme süreçlerinde en çok vakit kaybettiren şeylerden biri, kodun “bende çalışıyor” denen ama production’da patlayan halidir. CI/CD pipeline’ları tam olarak bu sorunu çözmek için var. GitLab CI/CD, bu konuda hem güçlü hem de kurulumu görece kolay bir araç sunuyor. Bu yazıda gerçek dünya senaryolarıyla, sıfırdan bir test ve build pipeline’ı nasıl kurarsınız, nelere dikkat etmeniz gerekir, bunları konuşacağız.

GitLab CI/CD Nedir ve Neden Kullanmalısınız

GitLab CI/CD, kodunuzu her push ettiğinizde otomatik olarak test eden, derleyen ve deploy edebilen entegre bir sistemdir. Ayrı bir Jenkins kurmanıza, harici bir servis bağlamanıza gerek yok. GitLab repo’nuzun içinde .gitlab-ci.yml dosyasını oluşturduğunuz anda sistem devreye giriyor.

Pratik avantajları şöyle sıralayabilirim:

  • Entegre yapı: Repo, CI/CD, container registry, artifact depolama tek çatı altında
  • GitLab Runner: Kendi sunucunuzda çalıştırabilir ya da GitLab’ın shared runner’larını kullanabilirsiniz
  • Paralel job çalıştırma: Stage’leri paralel çalıştırarak pipeline süresini kısaltabilirsiniz
  • Cache mekanizması: Dependency’leri cache’leyerek tekrar tekrar indirme derdinden kurtulursunuz
  • Artifacts: Job’lar arası dosya aktarımını yönetmek çok kolay

Tipik bir senaryoyu düşünelim: Bir Python web uygulaması geliştiriyorsunuz. Geliştirici kodu push ettiğinde otomatik olarak birim testleri çalışsın, kod kalite kontrolü yapılsın, Docker image build edilsin ve test ortamına deploy edilsin. İşte bu pipeline’ı adım adım kuracağız.

GitLab Runner Kurulumu

Pipeline’ların çalışabilmesi için bir Runner’a ihtiyacınız var. GitLab’ın shared runner’larını kullanabilirsiniz ama production ortamları için kendi runner’ınızı kurmak çok daha mantıklı. Hem kontrolünüz olur hem de shared runner kuyrukları sizi bekletmez.

Ubuntu üzerinde runner kurulumu şöyle yapılır:

# GitLab Runner paket deposunu 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

# Servis durumunu kontrol et
sudo systemctl status gitlab-runner

Kurulumdan sonra runner’ı GitLab instance’ınıza kaydetmeniz gerekiyor:

sudo gitlab-runner register 
  --url "https://gitlab.com/" 
  --registration-token "YOUR_REGISTRATION_TOKEN" 
  --executor "docker" 
  --docker-image "alpine:latest" 
  --description "production-runner" 
  --tag-list "docker,linux,production" 
  --run-untagged="false" 
  --locked="false"

Kayıt token’ını GitLab’da Settings > CI/CD > Runners bölümünden alabilirsiniz. Executor olarak docker seçmenizi öneririm. Her job izole bir container içinde çalışır, sistem kirlenmez, dependency çakışmaları yaşamazsınız.

Runner konfigürasyonu /etc/gitlab-runner/config.toml dosyasında tutulur. Concurrent job sayısını artırmak istiyorsanız:

# config.toml dosyasını düzenle
sudo nano /etc/gitlab-runner/config.toml

# concurrent değerini artır
# concurrent = 4

.gitlab-ci.yml Dosyasının Anatomisi

Pipeline’ın kalbi .gitlab-ci.yml dosyasıdır. Repo’nun root dizinine koyulur. Temel yapıyı anlayalım:

# Global değişkenler
variables:
  APP_NAME: "my-python-app"
  DOCKER_REGISTRY: "registry.gitlab.com/mygroup/my-python-app"
  PYTHON_VERSION: "3.11"

# Pipeline stage'lerini tanımla
stages:
  - validate
  - test
  - build
  - deploy

# Tüm job'lara uygulanacak default ayarlar
default:
  image: python:3.11-slim
  before_script:
    - pip install --upgrade pip
    - pip install -r requirements.txt

stages anahtar kelimesi pipeline’ın akışını belirler. Bir stage’deki tüm job’lar tamamlanmadan bir sonraki stage başlamaz. Aynı stage içindeki job’lar ise paralel çalışır.

Validate Stage: Kod Kalite Kontrolleri

Pipeline’ın ilk aşaması genellikle hızlı ve ucuz kontroller içermeli. Kod formatı, syntax hatası, güvenlik açıkları burada yakalanmalı.

# Kod stil kontrolü
lint:
  stage: validate
  script:
    - pip install flake8 black isort
    - flake8 src/ --max-line-length=100 --exclude=migrations
    - black --check src/
    - isort --check-only src/
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH == "develop"'

# Güvenlik açığı taraması
security-scan:
  stage: validate
  script:
    - pip install bandit safety
    - bandit -r src/ -ll
    - safety check --json
  allow_failure: true
  artifacts:
    reports:
      junit: security-report.xml
    expire_in: 1 week

Burada dikkat edilmesi gereken birkaç nokta var. rules kısmı ile hangi durumlarda bu job’un çalışacağını belirliyoruz. Merge request açıldığında, main veya develop branch’e push geldiğinde çalışsın istiyoruz. allow_failure: true ile güvenlik taramasının pipeline’ı bloklamamasını sağlıyoruz ama sonuçları yine de görüyoruz.

Test Stage: Unit ve Integration Testler

Test stage’i pipeline’ın en kritik parçası. Burada hem hız hem de kapsam önemli.

# Unit testler
unit-tests:
  stage: test
  services:
    - name: postgres:14
      alias: db
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: testuser
    POSTGRES_PASSWORD: testpass
    DATABASE_URL: "postgresql://testuser:testpass@db/testdb"
  script:
    - pip install pytest pytest-cov pytest-django
    - pytest tests/unit/ -v --cov=src --cov-report=xml --cov-report=term-missing
  coverage: '/TOTAL.*s+(d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
      junit: test-results.xml
    expire_in: 30 days

# Integration testler - sadece main ve develop'ta çalışsın
integration-tests:
  stage: test
  services:
    - name: postgres:14
      alias: db
    - name: redis:7
      alias: cache
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: testuser
    POSTGRES_PASSWORD: testpass
    REDIS_URL: "redis://cache:6379"
    DATABASE_URL: "postgresql://testuser:testpass@db/testdb"
  script:
    - pytest tests/integration/ -v --timeout=60
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH == "develop"'
  allow_failure: false

services kısmı harika bir özellik. Test için ihtiyaç duyduğunuz PostgreSQL, Redis gibi servisleri job boyunca ayağa kaldırıyor. Job bitince yok oluyor. Ayrı bir test veritabanı sunucusu yönetmekten kurtuluyorsunuz.

coverage satırı önemli. GitLab bu regex ile test çıktısından coverage yüzdesini çekiyor ve MR’larda gösteriyor. Bu sayede “coverage düştü mü yükseldi mi?” sorusunun cevabını merge etmeden önce görüyorsunuz.

Cache Kullanımı: Pipeline Hızlandırma

Her job’da pip install çalıştırmak vakit kaybı. Cache mekanizmasıyla bunu büyük ölçüde azaltabilirsiniz:

# Cache konfigürasyonu - global veya per-job tanımlanabilir
variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key:
    files:
      - requirements.txt
  paths:
    - .cache/pip
    - venv/

# Job'a özel cache override
unit-tests:
  stage: test
  cache:
    key:
      files:
        - requirements.txt
    paths:
      - .cache/pip
    policy: pull  # Sadece cache'den oku, yazma
  before_script:
    - python -m venv venv
    - source venv/bin/activate
    - pip install -r requirements.txt
  script:
    - pytest tests/unit/

policy: pull ile cache’i sadece okuyup yazmayan job’lar yapılandırabilirsiniz. Sadece build job’u cache’i güncellesin, test job’ları sadece okusun gibi bir strateji performansı artırır.

Cache key’ini requirements.txt dosyasına bağlamak da akıllıca bir yaklaşım. Dosya değişmediğinde cache geçerli kalıyor, değiştiğinde otomatik olarak yenileniyor.

Build Stage: Docker Image Oluşturma

Test’ler geçtikten sonra sıra Docker image build etmeye geliyor:

build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
  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
        --build-arg VERSION=$CI_COMMIT_REF_NAME
        --cache-from $IMAGE_LATEST
        --tag $IMAGE_TAG
        --tag $IMAGE_LATEST
        .
    - docker push $IMAGE_TAG
    - docker push $IMAGE_LATEST
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH == "develop"'
  artifacts:
    reports:
      dotenv: build.env
  after_script:
    - echo "IMAGE_TAG=$IMAGE_TAG" >> build.env
    - echo "Build tamamlandi - $IMAGE_TAG"

$CI_REGISTRY_IMAGE, $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD gibi değişkenler GitLab tarafından otomatik olarak sağlanıyor. Bunları manuel ayarlamanıza gerek yok.

--cache-from $IMAGE_LATEST parametresi önemli. Docker build cache’i layer bazlı kullanıyor. Önceki build’in latest tag’ini cache kaynağı olarak kullandığınızda, değişmeyen layer’lar yeniden build edilmiyor. Özellikle büyük base image’larda ciddi zaman kazancı sağlıyor.

dotenv artifact’ı da dikkat çekici bir özellik. Build job’unda üretilen değişkenleri sonraki job’lara aktarmak için kullanılıyor. Deploy job’u hangi image’ı deploy edeceğini bu sayede öğrenebilir.

Multi-Stage Dockerfile ile Uyumlu Çalışma

GitLab CI/CD ile beraber kullanacağınız Dockerfile’ın da temiz olması gerekiyor:

# Build stage
FROM python:3.11-slim AS builder

WORKDIR /app

# Sadece dependency dosyalarını kopyala
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Production stage
FROM python:3.11-slim AS production

WORKDIR /app

# Build stage'den sadece kurulu paketleri al
COPY --from=builder /root/.local /root/.local

# Uygulama kodunu kopyala
COPY src/ ./src/

# Build argümanlarını label olarak kaydet
ARG BUILD_DATE
ARG GIT_COMMIT
ARG VERSION

LABEL org.opencontainers.image.created=$BUILD_DATE 
      org.opencontainers.image.revision=$GIT_COMMIT 
      org.opencontainers.image.version=$VERSION

# Root olmayan kullanıcıyla çalıştır
RUN useradd --create-home appuser
USER appuser

ENV PATH=/root/.local/bin:$PATH

EXPOSE 8000
CMD ["gunicorn", "src.wsgi:application", "--bind", "0.0.0.0:8000"]

Multi-stage build kullanmak image boyutunu dramatik şekilde küçültüyor. Builder stage’de compiler, build araçları ve benzer şeyler bulunuyor ama final image’a gelmiyor. Security açısından da faydalı.

Deploy Stage ve Environment Yönetimi

Build tamamlandıktan sonra deploy aşaması geliyor. Farklı environment’lar için farklı stratejiler kullanabilirsiniz:

# Staging deploy - develop branch'te otomatik
deploy-staging:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.myapp.com
  variables:
    KUBECONFIG: $STAGING_KUBECONFIG
  before_script:
    - echo $STAGING_KUBECONFIG | base64 -d > kubeconfig.yaml
    - export KUBECONFIG=kubeconfig.yaml
  script:
    - kubectl set image deployment/myapp
        app=$IMAGE_TAG
        -n staging
    - kubectl rollout status deployment/myapp -n staging --timeout=120s
  dependencies:
    - build-image
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

# Production deploy - main branch'te manuel onay gerekli
deploy-production:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://myapp.com
  variables:
    KUBECONFIG: $PRODUCTION_KUBECONFIG
  before_script:
    - echo $PRODUCTION_KUBECONFIG | base64 -d > kubeconfig.yaml
    - export KUBECONFIG=kubeconfig.yaml
  script:
    - kubectl set image deployment/myapp
        app=$IMAGE_TAG
        -n production
    - kubectl rollout status deployment/myapp -n production --timeout=180s
  dependencies:
    - build-image
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  allow_failure: false

Production deploy’da when: manual kullanmak sizi gereksiz kazalardan koruyor. Pipeline otomatik ilerliyor ama son adımda bir insan onayı gerekiyor. GitLab arayüzünde yeşil bir “Play” butonu beliriyor ve yetkili kişi tıklayana kadar deploy gerçekleşmiyor.

environment bloğu da önemli. GitLab bu sayede hangi commit’in hangi ortamda çalıştığını takip ediyor. Deployments bölümünde geçmiş deploy’ları görebilir, rollback yapabilirsiniz.

Ortam Değişkenleri ve Güvenlik

Pipeline’da şifreler, API anahtarları gibi hassas bilgileri yönetmek kritik. .gitlab-ci.yml dosyasına asla şifre yazmayın.

GitLab’da Settings > CI/CD > Variables bölümünden değişken tanımlarsınız:

  • Protected: Sadece protected branch ve tag’lerde kullanılır
  • Masked: Log’larda görünmez, yıldızlarla maskelenir
  • File: Değişken bir dosya olarak job’a aktarılır, büyük sertifikalar için kullanışlı

Değişken gruplarını GitLab Group düzeyinde de tanımlayabilirsiniz. Birden fazla projeniz varsa ve aynı değişkeni paylaşıyorlarsa grup düzeyindeki değişkenler hayat kurtarır.

# Değişken kullanımı - şifreler environment'tan gelir
deploy-staging:
  stage: deploy
  script:
    - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD registry.example.com
    - ansible-playbook deploy.yml
        -e "app_version=$CI_COMMIT_SHA"
        -e "db_password=$DB_PASSWORD"
        -e "api_key=$EXTERNAL_API_KEY"

Pipeline Optimizasyonu: Gerçek Dünya İpuçları

Zamanla pipeline’lar şişiyor ve yavaşlıyor. Birkaç pratik optimizasyon:

Sadece değişen dosyalar için job çalıştırma:

frontend-tests:
  stage: test
  script:
    - cd frontend && npm test
  rules:
    - changes:
        - frontend/**/*
        - package.json

changes kuralı sayesinde frontend dosyaları değişmemişse bu job hiç çalışmıyor. Backend geliştiricisi Python dosyasını değiştirdiğinde frontend testleri gereksiz yere çalışmıyor.

Parallel job’lar ile test hızlandırma:

unit-tests:
  stage: test
  parallel: 4
  script:
    - pytest tests/unit/ --splits $CI_NODE_TOTAL --group $CI_NODE_INDEX

parallel: 4 ile aynı job dört ayrı instance’ta çalışıyor. pytest-split gibi kütüphanelerle testleri otomatik bölerek her instance farklı test grubunu koşuyor. Dört kez hızlanıyor.

Fail fast stratejisi:

lint:
  stage: validate
  interruptible: true
  script:
    - flake8 src/

interruptible: true ile yeni bir commit geldiğinde eski pipeline otomatik iptal ediliyor. Runner kaynakları boşa gitmiyor.

Merge Request Pipeline’ları

GitLab’ın en güçlü özelliklerinden biri MR pipeline’larıdır. Merge etmeden önce kod kalitesini garantilemek için ideal.

# Sadece MR'da çalışan job
code-review-checks:
  stage: validate
  script:
    - pip install pylint
    - pylint src/ --fail-under=8.0
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

# MR'da çalışıp sonucu MR sayfasında gösteren test raporu
test-report:
  stage: test
  script:
    - pytest --junitxml=report.xml
  artifacts:
    when: always
    reports:
      junit: report.xml

junit artifact formatıyla test sonuçları doğrudan MR sayfasında görünüyor. Hangi testlerin geçtiği, hangilerinin başarısız olduğu, ne kadar sürdüğü anında görülüyor. Code review sürecini büyük ölçüde kolaylaştırıyor.

Scheduled Pipeline ve Otomatik Görevler

Her gün gece yarısı çalışan maintenance job’ları, haftalık güvenlik taramaları ya da aylık rapor üretme işlemleri için scheduled pipeline kullanabilirsiniz. GitLab’da CI/CD > Schedules bölümünden tanımlarsınız.

# Sadece schedule ile tetiklendiğinde çalışır
nightly-security-scan:
  stage: validate
  script:
    - pip install safety
    - safety check
    - pip install pip-audit
    - pip-audit
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

# Her commit'te de çalışır ama schedule'da daha kapsamlı
dependency-check:
  stage: validate
  script:
    - |
      if [ "$CI_PIPELINE_SOURCE" == "schedule" ]; then
        echo "Kapsamli tarama yapiliyor..."
        pip install pip-audit
        pip-audit --format json --output audit-report.json
      else
        echo "Hizli tarama yapiliyor..."
        pip install safety
        safety check
      fi
  artifacts:
    paths:
      - audit-report.json
    expire_in: 1 month
    when: always

Sonuç

GitLab CI/CD pipeline kurmak başta karmaşık görünebilir ama temel yapıyı anladıktan sonra her şey yerine oturuyor. En önemli nokta, pipeline’ı hayatı kolaylaştıracak şekilde tasarlamak. Çok katı kurallar koymak geliştiricileri kaçırır, çok gevşek kurallar ise CI/CD’nin amacını ortadan kaldırır.

Pratikte işe yarayan birkaç prensibi özetleyeyim:

  • Hızlı geri bildirim: Lint ve basit kontroller hızlı çalışmalı, geliştiricinin 2 dakika içinde hata görmesi gerekiyor
  • Kademeli kontrol: Her stage bir öncekini tamamlamalı, başarısız test varsa build yapma
  • Cache agresif kullan: Pipeline süresi uzadıkça kimse beklemek istemez
  • Ortam ayrımı: Staging otomatik, production manuel onaylı olsun
  • Secrets yönetimi: Hiçbir şifreyi dosyaya yazma, GitLab Variables kullan
  • Artifact saklama: Log ve raporları sakla ama expire_in koy, disk şişmesin

Bu pipeline yapısını bir kez oturttuğunuzda, “bende çalışıyor” problemi tarih oluyor. Her merge request için güvenilir bir kontrol katmanınız oluyor ve production’a ne deploy ettiğinizi tam olarak biliyorsunuz. Sistemin geri kalanını otomatize etmek için sağlam bir temel bu.

Bir yanıt yazın

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