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: &notify
  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ı (&notify 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:

  • cache kullanın, artifacts boyutlarını küçük tutun
  • parallel keyword’üyle testleri dağıtın
  • Gereksiz stage’leri birleştirin
  • needs keyword’ü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.

Bir yanıt yazın

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