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.
