Otomatik Deployment: GitLab CI/CD Rehberi
Üretim ortamına kod göndermek her zaman biraz sinir bozucu olmuştur. Eski yöntemle düşünün: geliştirici kodu yazıyor, test ediyor (bazen), FTP ile sunucuya atıyor, bir şeyler patlıyor, gece 2’de uyandırılan sysadmin ekibi devreye giriyor. Bu döngüyü kırmak için CI/CD pipeline’ları hayatımıza girdi ve GitLab, bu konuda gerçekten güçlü bir araç sunuyor. Bu yazıda sıfırdan başlayarak production-ready bir GitLab CI/CD pipeline’ı nasıl kurarsınız, hangi tuzaklardan kaçınırsınız, gerçek dünya senaryolarıyla birlikte anlatacağım.
GitLab CI/CD Nedir ve Neden Kullanmalısınız
GitLab CI/CD, kodunuzu otomatik olarak test eden, derleyen ve deploy eden entegre bir sistemdir. Ayrı bir araç kurmak zorunda değilsiniz çünkü GitLab’ın kendi içinde geliyor. Bir .gitlab-ci.yml dosyası oluşturuyorsunuz, commit atıyorsunuz ve geri kalanını sistem hallediyor.
Benim için en büyük avantajı şu: tek bir araçta hem kaynak kodu yönetimi hem CI/CD hem de container registry var. Jenkins kurmuş olanlar bilir, o ekosistemi ayakta tutmak başlı başına bir iş. GitLab ile bu karmaşıklığın büyük kısmından kurtuluyorsunuz.
Temel kavramlar şunlardır:
- Pipeline: Tüm otomasyonun ana yapısı, commit geldiğinde tetiklenen iş akışı
- Stage: Pipeline içindeki aşamalar (test, build, deploy gibi)
- Job: Her stage içindeki spesifik görevler
- Runner: Job’ları çalıştıran ajan yazılımı
- Artifact: Job’lar arasında aktarılan dosyalar
GitLab Runner Kurulumu
Pipeline’lar çalışabilmek için bir runner’a ihtiyaç duyar. GitLab’ın shared runner’larını kullanabilirsiniz, ancak production ortamları için kendi runner’ınızı kurmanızı şiddetle tavsiye ederim. Hem güvenlik hem de performans açısından çok daha iyi.
Ubuntu/Debian üzerinde runner kurulumu şöyle yapılır:
# GitLab Runner repo'yu 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
# Servisi başlat
sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runner
# Runner'ı kaydet (GitLab arayüzünden token alın)
sudo gitlab-runner register
--non-interactive
--url "https://gitlab.com/"
--registration-token "YOUR_TOKEN_HERE"
--executor "docker"
--docker-image alpine:latest
--description "production-runner"
--tag-list "production,deploy"
--run-untagged="false"
--locked="false"
Runner’ı kaydettikten sonra /etc/gitlab-runner/config.toml dosyasını düzenlemenizi öneririm. Özellikle concurrent değerini sunucunuzun kapasitesine göre ayarlayın. Varsayılan olarak 1 geliyor, çok küçük bir değer.
sudo nano /etc/gitlab-runner/config.toml
concurrent = 4
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "production-runner"
url = "https://gitlab.com/"
token = "TOKEN"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "alpine:latest"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
İlk .gitlab-ci.yml Dosyası
Şimdi işin özüne gelelim. .gitlab-ci.yml dosyası projenizin kök dizininde yaşar ve pipeline’ınızın tüm mantığını içerir.
Basit bir Node.js uygulaması için başlangıç noktası:
image: node:18-alpine
stages:
- install
- test
- build
- deploy
variables:
NODE_ENV: "production"
APP_NAME: "myapp"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
install_dependencies:
stage: install
script:
- npm ci --prefer-offline
artifacts:
paths:
- node_modules/
expire_in: 1 hour
run_tests:
stage: test
script:
- npm run test:unit
- npm run test:integration
coverage: '/Coverage: d+.d+/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build_application:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy_staging:
stage: deploy
environment:
name: staging
url: https://staging.myapp.com
script:
- echo "Staging'e deploy ediliyor..."
- rsync -avz --delete dist/ deploy@staging-server:/var/www/myapp/
only:
- develop
deploy_production:
stage: deploy
environment:
name: production
url: https://myapp.com
script:
- echo "Production'a deploy ediliyor..."
- rsync -avz --delete dist/ deploy@prod-server:/var/www/myapp/
only:
- main
when: manual
Burada dikkat etmenizi istediğim birkaç şey var. when: manual production deploy’unu otomatik yapmıyor, bir insan onayı istiyor. Bu iyi bir pratik. Ayrıca only direktifiyle hangi branch’in nereye deploy edileceğini kontrol ediyoruz.
Docker ile Build ve Registry Kullanımı
Gerçek dünyada çoğu proje artık Docker container’larıyla deploy ediliyor. GitLab kendi container registry’sini sunuyor ve bunu kullanmak işleri ciddi anlamda kolaylaştırıyor.
image: docker:24.0.5
services:
- docker:24.0.5-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
stages:
- test
- build
- push
- deploy
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build_image:
stage: build
script:
- docker build -t $IMAGE_TAG -t $IMAGE_LATEST .
- docker push $IMAGE_TAG
- docker push $IMAGE_LATEST
only:
- main
- develop
deploy_to_server:
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 "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $IMAGE_TAG &&
docker stop myapp || true &&
docker rm myapp || true &&
docker run -d --name myapp --restart always -p 8080:8080 $IMAGE_TAG
"
environment:
name: production
only:
- main
when: manual
Bu pipeline’da SSH_PRIVATE_KEY ve PRODUCTION_SERVER gibi değişkenler GitLab’ın CI/CD Settings bölümünden tanımlanıyor. Bu değişkenleri asla .gitlab-ci.yml dosyasına yazmayın. Bir sysadmin olarak bu kuralı çiğneyen kaç geliştiricinin başını yaktığını gördüm.
Secret Yönetimi ve Güvenlik Pratikleri
Pipeline güvenliği ciddiye alınması gereken bir konu. GitLab’da secret’ları şöyle yönetirsiniz:
Proje Ayarları üzerinden:
- Settings > CI/CD > Variables bölümüne gidin
- “Protected” işaretliyseniz sadece protected branch’lerde çalışır
- “Masked” işaretliyseniz log’larda görünmez
# Örnek: SSH key oluşturma ve GitLab'a ekleme
ssh-keygen -t ed25519 -C "gitlab-ci-deploy" -f ~/.ssh/gitlab_deploy -N ""
# Public key'i sunucuya ekle
cat ~/.ssh/gitlab_deploy.pub >> ~/.ssh/authorized_keys
# Private key'i GitLab'a ekle (SSH_PRIVATE_KEY olarak)
cat ~/.ssh/gitlab_deploy
Bir projede production veritabanı şifresi .gitlab-ci.yml dosyasına hardcode edilmiş görmüştüm. Kodu incelerken fark ettim, commit geçmişi temizlenmeden önce o şifreyle kim ne yaptı bilmiyoruz. Bu tür durumlar için GitLab’ın vault entegrasyonunu veya en azından masked variable’ları kullanın.
Gerçek Dünya Senaryosu: Bir E-ticaret Sitesi Pipeline’ı
Diyelim ki bir e-ticaret sitesi için tam bir pipeline kuruyorsunuz. PHP/Laravel backend, Node.js frontend ve PostgreSQL veritabanı var. İşte bu senaryo için geliştirilmiş bir yapı:
stages:
- validate
- test
- security
- build
- staging
- production
variables:
PHP_VERSION: "8.2"
NODE_VERSION: "18"
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
# PHP Backend Testleri
php_tests:
stage: test
image: php:8.2-cli
services:
- postgres:15-alpine
variables:
DATABASE_URL: "postgresql://test_user:test_pass@postgres/test_db"
before_script:
- apt-get update && apt-get install -y libpq-dev
- docker-php-ext-install pdo_pgsql
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --no-dev --optimize-autoloader
script:
- php artisan test --parallel
- php artisan migrate --force
coverage: '/^s*Lines:s*d+.d+%/'
# Güvenlik taraması
security_scan:
stage: security
image: owasp/dependency-check:latest
script:
- /usr/share/dependency-check/bin/dependency-check.sh
--project "ecommerce"
--scan ./
--format JSON
--out reports/
artifacts:
reports:
dependency_scanning: reports/dependency-check-report.json
expire_in: 1 week
allow_failure: true
# Frontend build
frontend_build:
stage: build
image: node:18-alpine
cache:
key: npm-$CI_COMMIT_REF_SLUG
paths:
- frontend/node_modules/
script:
- cd frontend
- npm ci
- npm run build:production
artifacts:
paths:
- frontend/dist/
expire_in: 1 day
# Staging deploy
deploy_staging:
stage: staging
image: alpine:latest
environment:
name: staging
url: https://staging.shop.com
before_script:
- apk add --no-cache rsync openssh-client
- eval $(ssh-agent -s)
- echo "$STAGING_SSH_KEY" | tr -d 'r' | ssh-add -
- mkdir -p ~/.ssh && ssh-keyscan -H $STAGING_HOST >> ~/.ssh/known_hosts
script:
- rsync -avz --delete
--exclude='.env'
--exclude='storage/logs'
./ deploy@$STAGING_HOST:/var/www/staging/
- ssh deploy@$STAGING_HOST "cd /var/www/staging && php artisan migrate --force && php artisan cache:clear"
only:
- develop
Pipeline Optimizasyonu ve Cache Kullanımı
Yavaş pipeline’lar geliştiricileri çıldırtır. 20 dakika süren bir pipeline varsa insanlar bypass yolları arar, bu da güvenlik açıklarına yol açar. Cache kullanımı bu sorunu büyük ölçüde çözer.
# Gelişmiş cache stratejisi
cache:
- key:
files:
- package-lock.json
prefix: npm
paths:
- node_modules/
policy: pull-push
- key:
files:
- composer.lock
prefix: composer
paths:
- vendor/
policy: pull-push
# Paralel job'lar
test_unit:
stage: test
parallel: 3
script:
- npm run test:unit -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
# Koşullu job çalıştırma
test_e2e:
stage: test
script:
- npm run test:e2e
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- src/**/*
- tests/**/*
rules direktifi only/except‘ten çok daha güçlü. Değişen dosyalara göre job çalıştırabiliyorsunuz. Sadece frontend dosyaları değişmişse neden backend testlerini koşturuyorsunuz ki?
Rollback Stratejisi
Her deployment planının bir rollback stratejisi olmalı. GitLab ile bunu şöyle yapılandırabilirsiniz:
deploy_production:
stage: production
script:
- ssh deploy@$PROD_HOST "
cd /var/www/app &&
cp -r current current_backup_$(date +%Y%m%d_%H%M%S) &&
rsync -avz --delete $CI_PROJECT_DIR/dist/ current/ &&
php artisan migrate --force ||
(echo 'Deploy başarısız, geri alınıyor...' &&
rm -rf current &&
mv current_backup_* current &&
exit 1)
"
environment:
name: production
on_stop: rollback_production
rollback_production:
stage: production
script:
- ssh deploy@$PROD_HOST "
cd /var/www/app &&
BACKUP=$(ls -t | grep current_backup | head -1) &&
rm -rf current &&
mv $BACKUP current &&
echo 'Rollback tamamlandı: '$BACKUP
"
environment:
name: production
action: stop
when: manual
only:
- main
Rollback’ı on_stop ile tanımlamak GitLab’ın environment arayüzünde bir “Stop” butonu oluşturuyor. Bir şeyler ters gittiğinde ops ekibi kod yazmadan rollback yapabiliyor.
Monitoring ve Bildirimler
Pipeline başarısız olduğunda haberdar olmak istiyorsunuz. Slack entegrasyonu için:
.notify_template: ¬ify
after_script:
- |
if [ "$CI_JOB_STATUS" == "failed" ]; then
curl -X POST -H 'Content-type: application/json'
--data "{
"text": "Pipeline başarısız oldu!",
"attachments": [{
"color": "danger",
"fields": [
{"title": "Proje", "value": "$CI_PROJECT_NAME", "short": true},
{"title": "Branch", "value": "$CI_COMMIT_BRANCH", "short": true},
{"title": "Commit", "value": "$CI_COMMIT_SHORT_SHA", "short": true},
{"title": "Yazar", "value": "$CI_COMMIT_AUTHOR", "short": true}
]
}]
}"
$SLACK_WEBHOOK_URL
fi
deploy_production:
<<: *notify
stage: production
script:
- echo "Deploy ediliyor..."
YAML anchor kullanımı (¬ify ve <<: *notify) tekrar eden konfigürasyonları birleştirmenin güzel bir yolu.
Pipeline’ı Versiyonlamak ve Şablonlar
Birden fazla projeniz varsa her birine aynı .gitlab-ci.yml kopyalamak istemezsiniz. GitLab’ın include direktifi bu sorunu çözer:
# Ana pipeline dosyası
include:
- project: 'devops/ci-templates'
ref: main
file: '/templates/nodejs.yml'
- project: 'devops/ci-templates'
ref: main
file: '/templates/docker.yml'
- local: '.gitlab/custom-jobs.yml'
# Şablondan gelen job'ı override et
deploy_production:
extends: .deploy_template
environment:
name: production
variables:
DEPLOY_HOST: $PROD_HOST
only:
- main
Bu yaklaşımla merkezi bir template repository oluşturuyorsunuz, tüm projeler bundan besleniyoru ve güvenlik yamalarını tek bir yerden dağıtabiliyorsunuz.
Sık Karşılaşılan Hatalar ve Çözümleri
“No space left on device” hatası:
# Runner sunucusunda eski Docker image'larını temizle
sudo docker system prune -af --volumes
# Cron ile otomatikleştir
echo "0 3 * * 0 docker system prune -af --volumes >> /var/log/docker-cleanup.log 2>&1" | sudo crontab -
Pipeline çok yavaş çalışıyor:
cachekullanın,artifactsboyutlarını küçük tutunparallelkeyword’üyle testleri dağıtın- Gereksiz stage’leri birleştirin
needskeyword’üyle stage sırasını bypass edin
Runner registration sonrası job almıyor:
# Runner durumunu kontrol et
sudo gitlab-runner status
sudo gitlab-runner verify
# Log'lara bak
sudo journalctl -u gitlab-runner -f
Docker socket hatası (dind sorunları):
# DOCKER_TLS_CERTDIR değişkenini boş bırakın (daha az güvenli ama sorunsuz)
variables:
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://docker:2375
Sonuç
GitLab CI/CD’yi doğru kurmak başlangıçta vakit alıyor, ancak bu yatırımın geri dönüşü inanılmaz hızlı. Artık “prod’a el ile attım, umarım çalışır” diye telefonunuza bakmıyorsunuz gece yarısı. Her commit otomatik test ediliyor, staging’de doğrulanıyor ve production’a kontrollü bir şekilde gidiyor.
Başlarken aşırı karmaşık bir yapı kurmaya çalışmayın. Önce basit bir test-build-deploy döngüsü kurun, çalışır hale getirin, sonra üzerine ekleyin. Güvenlik taramaları, performans testleri, canary deployment gibi gelişmiş özellikler zamanla eklenebilir.
En kritik kural şu: pipeline’ınız production ortamınızı yansıtmalı. Test ortamında çalışıp prod’da çalışmayan bir pipeline sizi gece 2’de kurtarmaz. Docker kullanıyorsanız her yerde aynı image’ı kullanın, environment değişkenlerini standartlaştırın ve rollback stratejinizi önceden test edin.
Sorunla karşılaştığınızda CI_DEBUG_TRACE: "true" değişkenini ekleyerek tüm komutların çıktısını görebilirsiniz. Bu, özellikle SSH veya environment sorunlarını debug ederken hayat kurtarıcı oluyor.
