CI/CD Nedir: Yazılım Geliştirmede Otomasyon Rehberi

Bir sabah geldiniz, üretim ortamında uygulama çalışmıyor. Geçen gece bir geliştirici “küçük bir düzeltme” push’ladı, kimse test etmedi, kimse review yapmadı. Sonuç: 3 saatlik kesinti, sinirli müşteriler ve bir sürü “ben yapmadım” toplantısı. Bu senaryo tanıdık geliyor mu? İşte CI/CD tam olarak bu tür durumları önlemek için var.

CI/CD Nedir, Ne Değildir

CI/CD’yi “otomatik deployment aracı” olarak tanımlayanları sık görürüm. Bu tanım eksik. CI/CD bir kültür değişimidir, araçlar bu kültürü hayata geçirmenin yollarıdır.

Continuous Integration (Sürekli Entegrasyon): Geliştiricilerin kod değişikliklerini sık sık (günde birden fazla kez) ortak bir branch’e entegre etmesi ve her entegrasyonun otomatik build ve test süreçleriyle doğrulanması pratiğidir.

Continuous Delivery (Sürekli Teslimat): CI’ın üzerine inşa edilir. Her başarılı build’in üretim ortamına deploy edilebilir halde tutulmasını sağlar. Deploy kararı hâlâ manuel olabilir.

Continuous Deployment (Sürekli Dağıtım): Continuous Delivery’nin bir adım ötesi. Manuel onay olmadan, testleri geçen her değişiklik otomatik olarak üretime gider.

Hangisini seçeceğiniz, ekibinizin olgunluk seviyesine ve iş gereksinimlerinize bağlı. Fintech veya sağlık sektöründeyseniz Continuous Delivery daha uygun olabilir. SaaS ürün geliştiriyorsanız Continuous Deployment’a geçiş mantıklı.

Temel Kavramlar ve Pipeline Anatomisi

Bir CI/CD pipeline’ı birbirini takip eden aşamalardan oluşur. Her aşama başarısız olursa sonraki aşama çalışmaz. Bu fail-fast prensibi, sorunları mümkün olan en erken noktada yakalamayı sağlar.

Tipik bir pipeline şu aşamalardan geçer:

  • Source: Kod değişikliği tetikleyici olur (git push, PR oluşturma)
  • Build: Kod derlenir, bağımlılıklar çekilir
  • Test: Unit testler, integration testler, güvenlik taramaları
  • Artifact: Build çıktısı paketlenir ve saklanır
  • Deploy to Staging: Test ortamına deployment
  • Integration/E2E Tests: Gerçek ortama yakın testler
  • Deploy to Production: Üretim ortamına deployment

Her aşamanın ne kadar sürdüğü kritik. 45 dakika süren bir pipeline, geliştiricilerin CI’ı atlamaya çalışmasına yol açar. Hedef 10-15 dakikanın altında tutmak.

GitHub Actions ile Pratik Başlangıç

En yaygın kullanılan araçlardan biri GitHub Actions. Minimal bir yapılandırmayla işe koyulalım:

# .github/workflows/ci.yml

name: CI Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Kodu çek
        uses: actions/checkout@v4
      
      - name: Node.js kurulumu
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Bağımlılıkları yükle
        run: npm ci
      
      - name: Testleri çalıştır
        run: npm test
      
      - name: Build al
        run: npm run build

Bu basit yapı bile ekibinize çok şey kazandırır. PR açıldığında testler otomatik koşar, main branch’e bozuk kod gitmez.

Jenkins ile Daha Karmaşık Senaryolar

Kurumsal ortamlarda Jenkins hâlâ çok yaygın. Özellikle on-premise gereksinimler ve gelişmiş özelleştirme ihtiyacı olduğunda tercih ediliyor. Bir Jenkinsfile örneğine bakalım:

// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'maven:3.9-openjdk-17'
            args '-v /root/.m2:/root/.m2'
        }
    }
    
    environment {
        DOCKER_REGISTRY = 'registry.sirket.com'
        APP_NAME = 'uygulama-adi'
        VERSION = "${env.BUILD_NUMBER}"
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'mvn test -Dtest=UnitTests'
                    }
                }
                stage('Integration Tests') {
                    steps {
                        sh 'mvn test -Dtest=IntegrationTests'
                    }
                }
            }
        }
        
        stage('Docker Build & Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'registry-credentials',
                    usernameVariable: 'REG_USER',
                    passwordVariable: 'REG_PASS'
                )]) {
                    sh """
                        docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} .
                        docker login ${DOCKER_REGISTRY} -u ${REG_USER} -p ${REG_PASS}
                        docker push ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}
                    """
                }
            }
        }
        
        stage('Deploy to Staging') {
            steps {
                sh """
                    kubectl set image deployment/${APP_NAME} 
                        ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} 
                        -n staging
                    kubectl rollout status deployment/${APP_NAME} -n staging
                """
            }
        }
    }
    
    post {
        failure {
            slackSend(
                channel: '#devops-alerts',
                color: 'danger',
                message: "Pipeline başarısız: ${env.JOB_NAME} - Build #${env.BUILD_NUMBER}"
            )
        }
        success {
            echo "Pipeline tamamlandı: ${env.BUILD_NUMBER}"
        }
    }
}

Parallel stage kullanımına dikkat edin. Unit ve integration testleri paralel çalıştırarak toplam süreyi düşürüyoruz. Büyük projelerde bu fark saatler yaratabilir.

Docker ile Tekrarlanabilir Build Ortamları

“Bende çalışıyor” problemi sysadminlerin kabusu. Docker, build ortamını standartlaştırır. Bir uygulama için çok aşamalı Dockerfile:

# Dockerfile

# Build aşaması
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production aşaması
FROM node:20-alpine AS production
WORKDIR /app

# Güvenlik için root olmayan kullanıcı
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

USER appuser

EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s 
    CMD wget -q -O /dev/null http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

Multi-stage build iki kritik fayda sağlar: Final image boyutu küçülür, build araçları production image’a taşınmaz. Güvenlik açısından da önemli.

GitLab CI ile Tam Entegre Pipeline

GitLab CI, özellikle self-hosted GitLab kullanan ekipler için son derece güçlü. .gitlab-ci.yml dosyasıyla tüm pipeline tanımlanır:

# .gitlab-ci.yml

stages:
  - build
  - test
  - security
  - deploy-staging
  - deploy-production

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

build:
  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 -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  only:
    - main
    - develop

unit-tests:
  stage: test
  image: python:3.11
  script:
    - pip install -r requirements-dev.txt
    - pytest tests/unit/ --cov=src --cov-report=xml
  coverage: '/TOTAL.*s+(d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

security-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_TAG
  allow_failure: false

deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.uygulamam.com
  script:
    - kubectl config set-cluster staging --server=$KUBE_URL
    - kubectl set image deployment/app app=$IMAGE_TAG -n staging
    - kubectl rollout status deployment/app -n staging --timeout=5m
  only:
    - develop

deploy-production:
  stage: deploy-production
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://uygulamam.com
  script:
    - kubectl set image deployment/app app=$IMAGE_TAG -n production
    - kubectl rollout status deployment/app -n production --timeout=10m
  when: manual
  only:
    - main

when: manual satırına dikkat. Production deployment için manuel onay gerekiyor. Bu Continuous Delivery yaklaşımı, kritik ortamlarda çok tercih ediliyor.

Güvenlik Taramaları ve SAST/DAST

CI/CD pipeline’ına güvenlik testlerini dahil etmek “DevSecOps” anlayışının temelidir. Birçok ekip bunu hâlâ ihmal ediyor ve üretimde açık bulunduğunda panikliyor.

# Trivy ile container güvenlik taraması
trivy image --format json --output trivy-report.json myapp:latest

# Sonuçları filtrele, sadece HIGH ve CRITICAL göster
trivy image 
    --severity HIGH,CRITICAL 
    --ignore-unfixed 
    --exit-code 1 
    myapp:latest

# OWASP ZAP ile basit DAST taraması
docker run -t owasp/zap2docker-stable zap-baseline.py 
    -t https://staging.uygulamam.com 
    -r zap-report.html 
    -I

Güvenlik taramaları pipeline’ı yavaşlatır diye devre dışı bırakmak ciddi hata. Bunu ayrı bir paralel aşama olarak çalıştırırsanız toplam süreye etkisi minimuma iner.

Ortam Değişkenleri ve Secrets Yönetimi

Secrets yönetimi CI/CD’nin en çok ihmal edilen konularından biri. .env dosyasını repoya koymak veya pipeline log’larına şifre yazdırmak güvenlik felaketi.

# HashiCorp Vault ile secret çekme örneği
# Pipeline içinde kullanım

export VAULT_ADDR='https://vault.sirket.com'
export VAULT_TOKEN=$(cat /vault-token)

# Uygulama için gerekli secret'ları çek
DB_PASSWORD=$(vault kv get -field=password secret/uygulama/db)
API_KEY=$(vault kv get -field=key secret/uygulama/api)

# Ortam değişkeni olarak kullanım, log'a yazdırma
export DB_PASSWORD
export API_KEY

# Uygulama deployment'ı
kubectl create secret generic app-secrets 
    --from-literal=db-password="$DB_PASSWORD" 
    --from-literal=api-key="$API_KEY" 
    -n production 
    --dry-run=client -o yaml | kubectl apply -f -

GitHub Actions veya GitLab CI kullanıyorsanız, platform’un kendi secret store’unu kullanın. Hassas değerleri hiçbir zaman pipeline çıktısında görünür hale getirmeyin.

Gerçek Dünya: Rollback Stratejisi

Pipeline başarıyla tamamlandı, production’a deploy ettiniz. 5 dakika sonra hata oranı fırladı. Ne yapacaksınız?

Önceden hazırlanmış bir rollback stratejisi olmaması, kesinti süresini 10 dakikadan 2 saate çıkarabilir. Kubernetes üzerinde hızlı rollback:

# Mevcut deployment geçmişini gör
kubectl rollout history deployment/uygulama -n production

# Son deployment önceki versiyona geri al
kubectl rollout undo deployment/uygulama -n production

# Belirli bir versiyona geri al
kubectl rollout undo deployment/uygulama 
    --to-revision=3 
    -n production

# Rollback durumunu takip et
kubectl rollout status deployment/uygulama -n production

# Mevcut pod'ları ve versiyonları kontrol et
kubectl get pods -n production 
    -o jsonpath='{range .items[*]}{.metadata.name}{"t"}{.spec.containers[0].image}{"n"}{end}'

Rollback’i de CI/CD pipeline’ının bir parçası yapabilirsiniz. Deployment sonrası otomatik smoke test çalıştırıp başarısız olursa rollback tetikleyebilirsiniz. Bu pattern’i production’da kullanan ekiplerden çok olumlu geri bildirim aldım.

Pipeline Performansını İyileştirme

Yavaş pipeline geliştiricileri sinir eder ve CI’ı atlatmaya iter. Hız optimizasyonu için bazı pratik teknikler:

Cache kullanımı: Bağımlılıkları her seferinde sıfırdan çekmek ciddi zaman kaybettirir. GitHub Actions’ta:

# Akıllı cache kullanımı
- name: npm cache
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

hashFiles kullanımı önemli. Lock dosyası değişmediğinde cache’den alır, değişince yeniden oluşturur. Akıllı cache stratejisi tek başına 3-5 dakika kazandırabilir.

Test paralelleştirme: Test süiti büyüdükçe paralel çalıştırmak şart.

Docker layer cache: Her build’de base image’ı çekmemek için:

docker build 
    --cache-from $DOCKER_REGISTRY/myapp:latest 
    --build-arg BUILDKIT_INLINE_CACHE=1 
    -t $DOCKER_REGISTRY/myapp:$VERSION 
    .

Değişiklik tabanlı tetikleme: Monorepo kullanıyorsanız sadece değişen servisin pipeline’ını tetikleyin. Tüm servisleri her push’ta build etmek kaynakları boşa harcar.

Monitoring ve Observability

Pipeline başarılı ama uygulama düzgün çalışıyor mu? Deployment sonrası izleme CI/CD’nin ayrılmaz parçası.

Pipeline içinde temel health check:

#!/bin/bash
# post-deploy-check.sh

UYGULAMA_URL="https://uygulamam.com"
BEKLENEN_HTTP_KODU=200
TIMEOUT=300  # 5 dakika
ARALIK=10    # 10 saniyede bir kontrol

baslangic_zamani=$(date +%s)

echo "Deployment sonrası health check başlıyor..."

while true; do
    http_kodu=$(curl -s -o /dev/null -w "%{http_code}" 
        --max-time 10 
        "$UYGULAMA_URL/health")
    
    if [ "$http_kodu" -eq "$BEKLENEN_HTTP_KODU" ]; then
        echo "Uygulama sağlıklı çalışıyor (HTTP $http_kodu)"
        exit 0
    fi
    
    gecen_sure=$(( $(date +%s) - baslangic_zamani ))
    
    if [ "$gecen_sure" -ge "$TIMEOUT" ]; then
        echo "HATA: Timeout aşıldı. HTTP kodu: $http_kodu"
        echo "Rollback tetikleniyor..."
        kubectl rollout undo deployment/uygulama -n production
        exit 1
    fi
    
    echo "Bekleniyor... (${gecen_sure}s geçti, HTTP: $http_kodu)"
    sleep $ARALIK
done

Bu script, deployment sonrası uygulamanın ayağa kalkmasını bekler. Başaramazsa otomatik rollback yapar ve pipeline’ı başarısız işaretler.

Araç Seçimi: Neyi, Ne Zaman Kullanmalı

Hangi CI/CD aracını seçeceğiniz sorusu çok soruluyor. Net bir cevap vermek gerekirse:

  • GitHub Actions: GitHub kullanıyorsanız ve ekip küçükse ilk tercih. Ekstra altyapı gerektirmez, kurulumu hızlıdır.
  • GitLab CI: Self-hosted GitLab varsa en doğal entegrasyon. Kurumsal ortamlarda popüler.
  • Jenkins: Esneklik ve özelleştirme şartsa, on-premise zorunluysa. Yönetim maliyeti yüksek ama güçlü.
  • ArgoCD: Kubernetes-native GitOps yaklaşımı istiyorsanız. Jenkins veya GitHub Actions ile birlikte kullanılır.
  • Tekton: Cloud-native, Kubernetes üzerinde CI/CD pipeline’ları için. Öğrenme eğrisi dik.

Araç seçiminde en önemli kriter ekibin benimsemesi. En iyi araç, ekibin kullandığı araçtır.

Sonuç

CI/CD yolculuğu büyük ve karmaşık görünebilir. Ama şunu net söyleyeyim: Mükemmel pipeline’ı ilk günden kurmaya çalışmayın. Küçük başlayın.

İlk hafta sadece otomatik testleri koşturun. İkinci hafta build otomasyonu ekleyin. Bir ay sonra staging’e otomatik deploy. Üç ay sonra production deployment’ı düşünün.

En büyük hata, CI/CD’yi “devops ekibinin işi” olarak görmek. Geliştiriciler pipeline yazmalı, test yazmalı, sorunları çözmeli. Sysadmin ve DevOps mühendislerinin rolü altyapıyı sağlamak, en iyi pratikleri yaymak ve ekibe rehberlik etmek.

Pipeline bir kez kurulup unutulan bir sistem değil. Ekip büyüdükçe, uygulama karmaşıklaştıkça pipeline da evrilmeli. Düzenli retrospektifler yapın: pipeline ne kadar sürüyor, hangi aşamada en çok başarısız oluyoruz, neler iyileştirilebilir?

Kesintisiz delivery hedefi uzun bir yolculuk. Ama her adımda hem ekip hem de kullanıcılar bunun faydasını görüyor. Bir sabah ofise gelip “gece push ettim, sabah canlıya çıktı, hiçbir sorun yok” diyebilmek gerçekten güzel bir his.

Bir yanıt yazın

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