← Kursa Dön
📄 Text · 18 min

Production Deployment Checklist

Giriş

"Benim bilgisayarımda çalışıyor" — yazılım dünyasının en meşhur cümlesi. Ama production, sizin bilgisayarınız değildir. Production'da uygulamanız binlerce kullanıcıya hizmet verir, 7/24 çalışır, hatalar gerçek para kaybına dönüşür ve geri dönüşü zor olabilir.

Production deployment'ı bir uzay mekiği fırlatmasına benzetebilirsiniz. Roket mühendisleri fırlatma öncesi yüzlerce maddelik kontrol listesi uygular. Tek bir kaçırılan madde felaketle sonuçlanabilir. Yazılım deployment'ı bu kadar dramatik olmasa da, aynı disiplin gerektirir.

Bu derste Spring Boot uygulamasını production'a deploy etmek için gerekli her şeyi — health check'ler, graceful shutdown, JVM tuning, database migration, monitoring, logging, deployment stratejileri ve rollback planı — detaylı olarak inceleyeceğiz.

1. Health Check'ler — Uygulamanız Sağlıklı mı?

Health check, uygulamanızın "hayatta mı?" sorusuna yanıt verir. Kubernetes, Docker Swarm ve load balancer'lar bu endpoint'leri kullanarak trafik yönlendirmesi yapar.

Actuator Konfigürasyonu

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application-prod.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /internal/actuator
  endpoint:
    health:
      show-details: when_authorized
      show-components: when_authorized
      probes:
        enabled: true  # Kubernetes liveness/readiness probe'ları
  health:
    db:
      enabled: true
    redis:
      enabled: true
    diskSpace:
      enabled: true
      threshold: 1GB
  info:
    env:
      enabled: true
    git:
      mode: full

Custom Health Indicator

@Component
public class PaymentGatewayHealthIndicator
        implements HealthIndicator {

    private final PaymentGatewayClient paymentClient;

    public PaymentGatewayHealthIndicator(
            PaymentGatewayClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @Override
    public Health health() {
        try {
            long start = System.currentTimeMillis();
            boolean reachable = paymentClient.ping();
            long responseTime = System.currentTimeMillis() - start;

            if (reachable && responseTime < 5000) {
                return Health.up()
                    .withDetail("responseTime", responseTime + "ms")
                    .withDetail("gateway", "payment-api")
                    .build();
            } else if (reachable) {
                return Health.status("DEGRADED")
                    .withDetail("responseTime", responseTime + "ms")
                    .withDetail("warning", "Yavaş yanıt süresi")
                    .build();
            } else {
                return Health.down()
                    .withDetail("error", "Gateway erişilemiyor")
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .build();
        }
    }
}

Kubernetes Probe'ları

# Kubernetes deployment.yml
containers:
  - name: myapp
    image: myapp:1.0.0
    ports:
      - containerPort: 8080
    livenessProbe:
      httpGet:
        path: /internal/actuator/health/liveness
        port: 8080
      initialDelaySeconds: 60
      periodSeconds: 10
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /internal/actuator/health/readiness
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 5
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /internal/actuator/health/liveness
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 5
      failureThreshold: 30  # 10 + (30 * 5) = 160 saniye timeout
  • Liveness Probe: Uygulama yaşıyor mu? Başarısızsa container restart edilir

  • Readiness Probe: Uygulama istek kabul etmeye hazır mı? Başarısızsa trafik yönlendirilmez

  • Startup Probe: Uygulama başlama süreci tamamlandı mı? Spring Boot uygulamaları yavaş başlayabilir

⚠️ Dikkat: Liveness probe'da veritabanı kontrolü YAPMAYIN. Veritabanı geçici olarak erişilemezse tüm pod'lar restart edilir — bu durumu daha da kötüleştirir. Liveness = "JVM çalışıyor mu?", Readiness = "İstek kabul edebilir mi? (DB, cache dahil)".

2. Graceful Shutdown — Zarif Kapanma

Uygulama kapatılırken devam eden istekler yarıda kalmamamlıdır:

# application-prod.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Graceful shutdown süreci:

  1. Shutdown sinyali alınır (SIGTERM)

  2. Yeni istek kabul edilmesi durdurulur

  3. Devam eden isteklerin tamamlanması beklenir (30 saniye timeout)

  4. Tamamlanamayanlar 30 saniye sonra zorla kesilir

  5. Spring context kapatılır (bean'ler destroy edilir)

  6. JVM kapanır

// Shutdown sırasında temizlik yapılması gereken işler
@Component
@Slf4j
public class ShutdownHook implements DisposableBean {

    private final ExecutorService executorService;
    private final CacheManager cacheManager;

    @Override
    public void destroy() throws Exception {
        log.info("Shutdown started — cleaning up resources...");

        // Executor service'i kapat
        executorService.shutdown();
        if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
            executorService.shutdownNow();
        }

        // Cache'i flush et
        cacheManager.getCacheNames().forEach(name -> {
            Cache cache = cacheManager.getCache(name);
            if (cache != null) cache.clear();
        });

        log.info("Shutdown complete — all resources cleaned up");
    }
}

💡 İpucu: Kubernetes'te terminationGracePeriodSeconds değerini Spring Boot'un timeout-per-shutdown-phase değerinden yüksek tutun. Kubernetes 30 saniyede cevap alamazsa container'ı SIGKILL ile zorla kapatır.

3. JVM Tuning — Bellek ve Performans

Container-Aware JVM Ayarları

ENTRYPOINT ["java", \
    # GC Algoritması
    "-XX:+UseG1GC", \
    "-XX:MaxGCPauseMillis=200", \
    # Container bellek yönetimi
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    # OOM Durumu
    "-XX:+HeapDumpOnOutOfMemoryError", \
    "-XX:HeapDumpPath=/tmp/heapdump.hprof", \
    "-XX:+ExitOnOutOfMemoryError", \
    # Metaspace
    "-XX:MaxMetaspaceSize=256m", \
    # Startup optimizasyonu
    "-XX:TieredStopAtLevel=1", \
    # Random seed
    "-Djava.security.egd=file:/dev/./urandom", \
    "-jar", "app.jar"]

JVM Flag Açıklamaları

MaxRAMPercentage=75.0
├── Container'a 1GB bellek limiti verilmişse
├── Heap: 768 MB (%75)
└── Kalan: 256 MB (thread stack, metaspace, native memory, OS)

UseG1GC
├── Modern, düşük gecikme odaklı GC
├── Büyük heap'lerde (>4GB) iyi performans
└── MaxGCPauseMillis ile hedef pause süresi belirlenebilir

HeapDumpOnOutOfMemoryError
├── OOM olduğunda heap dump dosyası oluşturur
├── Post-mortem analiz için kritik
└── HeapDumpPath ile dump lokasyonunu belirtin

ExitOnOutOfMemoryError
├── OOM olduğunda JVM'i hemen sonlandırır
├── Kubernetes/Docker container'ı restart eder
└── Zombie process oluşmasını engeller

Farklı Ortamlar İçin JVM Profilleri

# docker-compose.yml
services:
  app-small:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512m    # MaxRAMPercentage=75 → Heap ~384MB
    environment:
      JAVA_OPTS: "-XX:+UseSerialGC"  # Küçük heap için

  app-large:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 4g      # MaxRAMPercentage=75 → Heap ~3GB
    environment:
      JAVA_OPTS: "-XX:+UseG1GC -XX:MaxGCPauseMillis=100"

4. Database Migration — Veritabanı Şema Yönetimi

Production'da ddl-auto: update kullanmak, canlı veritabanınızı riske atar. Şema değişiklikleri kontrollü, versiyonlanmış ve geri alınabilir olmalıdır.

Flyway Kurulumu

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<!-- PostgreSQL için ek -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-postgresql</artifactId>
</dependency>
# application-prod.yml
spring:
  jpa:
    hibernate:
      ddl-auto: validate  # Sadece doğrula, değiştirme!
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true

Migration Dosyaları

src/main/resources/db/migration/
├── V1__create_users_table.sql
├── V2__create_orders_table.sql
├── V3__add_email_index_to_users.sql
├── V4__add_shipping_address_to_orders.sql
└── V5__create_products_table.sql
-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);

-- V3__add_email_index_to_users.sql
-- Indeks zaten V1'de oluşturuldu ama örnek olarak:
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_lower
    ON users(LOWER(email));

⚠️ Dikkat: Flyway migration dosyaları bir kez çalıştırıldıktan sonra DEĞİŞTİRİLMEMELİDİR. Flyway, her dosyanın checksum'ını kontrol eder. Değişiklik yaparsanız uygulama başlamaz. Yeni bir migration dosyası oluşturun.

Migration Best Practices

-- ✅ Backward-compatible migration — eski kod hala çalışır
-- Adım 1: Yeni kolonu nullable ekle (eski kod etkilenmez)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Adım 2: Eski veriyi migrate et (ayrı migration)
UPDATE users SET phone = 'unknown' WHERE phone IS NULL;

-- Adım 3: NOT NULL constraint ekle (eski kod artık deploy edilmedi)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
-- ❌ Backward-incompatible — eski kod patlar
ALTER TABLE users DROP COLUMN old_field;  -- Eski kod bu kolonu kullanıyorsa PATLAK!
ALTER TABLE users RENAME COLUMN name TO full_name;  -- Aynı sorun

5. Logging — Production Loglama

Structured Logging (JSON)

<!-- logback-spring.xml -->
<configuration>
    <springProfile name="prod">
        <appender name="JSON_CONSOLE"
                  class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeMdcKeyName>traceId</includeMdcKeyName>
                <includeMdcKeyName>spanId</includeMdcKeyName>
                <includeMdcKeyName>userId</includeMdcKeyName>
                <fieldNames>
                    <timestamp>@timestamp</timestamp>
                    <version>[ignore]</version>
                </fieldNames>
            </encoder>
        </appender>

        <root level="INFO">
            <appender-ref ref="JSON_CONSOLE" />
        </root>

        <!-- Gereksiz noise'u kapat -->
        <logger name="org.hibernate" level="WARN" />
        <logger name="org.springframework" level="WARN" />
        <logger name="org.apache" level="WARN" />
    </springProfile>
</configuration>
# application-prod.yml
logging:
  level:
    root: INFO
    com.example: INFO
    org.hibernate.SQL: WARN
    org.springframework.web: WARN
  pattern:
    level: "%5p [${spring.application.name},%X{traceId},%X{spanId}]"

Log Yönetimi Kuralları

✅ Logla:
- İş olayları (sipariş oluşturuldu, ödeme alındı)
- Hatalar (exception, timeout, bağlantı hatası)
- Güvenlik olayları (başarısız login, yetkisiz erişim)
- Performans metrikleri (yavaş sorgu, yüksek latency)

❌ ASLA Loglama:
- Şifreler, API key'ler, token'lar
- Kredi kartı numaraları, kişisel veriler (KVKK/GDPR)
- Büyük request/response body'leri
- Her satır için DEBUG log (performans etkisi)

6. Monitoring ve Alerting

Prometheus + Micrometer

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: health,prometheus,metrics
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}
      environment: ${ENVIRONMENT:unknown}

Custom Business Metrics

@Service
@RequiredArgsConstructor
public class OrderService {

    private final MeterRegistry meterRegistry;
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.orderProcessingTimer = Timer.builder("order.processing.time")
            .description("Sipariş işleme süresi")
            .register(meterRegistry);
    }

    public OrderResponse createOrder(CreateOrderRequest request) {
        return orderProcessingTimer.record(() -> {
            // Sipariş oluştur
            Order order = processOrder(request);

            // İş metrikleri
            meterRegistry.counter("orders.created",
                "status", order.getStatus().name(),
                "payment_method", request.paymentMethod()
            ).increment();

            meterRegistry.gauge("orders.pending.count",
                orderRepository.countByStatus(OrderStatus.PENDING));

            return orderMapper.toResponse(order);
        });
    }
}

Alert Kuralları (Prometheus/Grafana)

# prometheus-alerts.yml
groups:
  - name: spring-boot-alerts
    rules:
      # Hata oranı yüksek
      - alert: HighErrorRate
        expr: rate(http_server_requests_seconds_count{status=~"5.."}[5m])
              / rate(http_server_requests_seconds_count[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Yüksek hata oranı: {{ $value | humanizePercentage }}"

      # JVM heap dolmak üzere
      - alert: JvmHeapHigh
        expr: jvm_memory_used_bytes{area="heap"}
              / jvm_memory_max_bytes{area="heap"} > 0.85
        for: 5m
        labels:
          severity: warning

      # Yanıt süresi yüksek
      - alert: HighLatency
        expr: histogram_quantile(0.99,
              rate(http_server_requests_seconds_bucket[5m])) > 2
        for: 5m
        labels:
          severity: warning

7. Deployment Stratejileri

Blue-Green Deployment

İki özdeş ortam: Blue (aktif) ve Green (yeni versiyon)

Adım 1: Green'e yeni versiyonu deploy et
Adım 2: Green'de smoke test yap
Adım 3: Load balancer'ı Green'e yönlendir
Adım 4: Sorun varsa anında Blue'ya geri dön (saniyeler)

┌─────────┐     ┌──────────────┐
│         │────▶│ Blue (v1.0)  │ ← Aktif
│  Load   │     └──────────────┘
│Balancer │     ┌──────────────┐
│         │     │ Green (v1.1) │ ← Hazırlanıyor
└─────────┘     └──────────────┘

Rolling Deployment

Instance'lar sırayla güncellenir, kesinti olmaz.

Adım 1: Instance 1'i traffic'ten çıkar
Adım 2: Instance 1'i güncelle ve health check bekle
Adım 3: Instance 1'i traffic'e al
Adım 4: Instance 2 için tekrarla

T=0: [v1.0] [v1.0] [v1.0] [v1.0]
T=1: [v1.1] [v1.0] [v1.0] [v1.0]
T=2: [v1.1] [v1.1] [v1.0] [v1.0]
T=3: [v1.1] [v1.1] [v1.1] [v1.0]
T=4: [v1.1] [v1.1] [v1.1] [v1.1]

Canary Deployment

Yeni versiyonu küçük bir kullanıcı grubuna sun, metrikleri izle.

%5 trafik  → v1.1 (Canary)
%95 trafik → v1.0 (Stable)

Metrikler iyiyse → %10, %25, %50, %100'e çıkar
Metrikler kötüyse → anında %0'a düşür (rollback)

8. Rollback Planı

Her deployment'ın bir rollback planı olmalıdır:

# Docker ile rollback
docker pull myapp:1.0.0  # Önceki versiyon
docker stop myapp-current
docker run -d --name myapp-rollback myapp:1.0.0

# Kubernetes ile rollback
kubectl rollout undo deployment/myapp
kubectl rollout undo deployment/myapp --to-revision=3

# Docker Compose ile rollback
# docker-compose.yml'daki image tag'i değiştir ve:
docker compose up -d

Rollback Koşulları

Aşağıdaki durumlardan BİRİ oluşursa rollback yapın:

  • Error rate %5'in üzerine çıktıysa

  • P99 latency 2 saniyeyi aştıysa

  • Health check başarısızsa

  • Kritik iş akışı (ödeme, kayıt) çalışmıyorsa

9. SSL/TLS Sertifikası

# application-prod.yml
server:
  port: 443
  ssl:
    enabled: true
    key-store: file:/etc/ssl/keystore.p12
    key-store-password: ${SSL_PASSWORD}
    key-store-type: PKCS12
    protocol: TLS
    enabled-protocols: TLSv1.3,TLSv1.2
# Let's Encrypt sertifikası oluşturma
certbot certonly --standalone -d api.example.com

# PKCS12 keystore'a dönüştürme
openssl pkcs12 -export \
    -in /etc/letsencrypt/live/api.example.com/fullchain.pem \
    -inkey /etc/letsencrypt/live/api.example.com/privkey.pem \
    -out keystore.p12 \
    -name myapp

10. Environment Variables Checklist

# Zorunlu production environment variables
DATABASE_URL=jdbc:postgresql://prod-db:5432/mydb
DB_USERNAME=app_readonly
DB_PASSWORD=<vault-managed>
JWT_SECRET=<256-bit-random>
SPRING_PROFILES_ACTIVE=prod

# Opsiyonel ama önerilen
JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75.0"
LOG_LEVEL=INFO
ENVIRONMENT=production
APP_VERSION=1.2.3

# Monitoring
ZIPKIN_URL=http://zipkin:9411
PROMETHEUS_ENABLED=true

Tam Deployment Checklist

☐ Pre-Deployment

  • ☐ Tüm testler geçiyor (unit + integration + e2e)

  • ☐ Code review tamamlandı

  • ☐ Security scan temiz (OWASP Dependency-Check)

  • ☐ Database migration staging'de test edildi

  • ☐ Rollback planı dokümante edildi

  • ☐ Tüm environment variable'lar set edildi

  • ☐ SSL/TLS sertifikası güncel (son kullanma tarihi > 30 gün)

  • ☐ Changelog/release notes hazır

☐ Deployment

  • ☐ Deployment stratejisi seçildi (blue-green / rolling / canary)

  • ☐ Database migration çalıştırıldı

  • ☐ Uygulama deploy edildi

  • ☐ Health check başarılı (/actuator/health → UP)

  • ☐ Smoke test geçti (kritik endpoint'ler çalışıyor)

☐ Post-Deployment (İlk 30 dakika)

  • ☐ Error rate normal (< %1)

  • ☐ Response time normal (P99 < 1s)

  • ☐ Memory/CPU metrikleri normal

  • ☐ Log'larda critical/error yok

  • ☐ İş metrikleri normal (sipariş, ödeme, kayıt sayıları)

  • ☐ Tüm entegrasyonlar çalışıyor (ödeme, email, SMS)

☐ Post-Deployment (24 saat)

  • ☐ Gece batch job'ları başarılı çalıştı

  • ☐ Memory leak belirtisi yok (heap sürekli artmıyor)

  • ☐ Connection pool leak yok

  • ☐ Önceki versiyon cleanup (eski container'lar, image'lar)

Özet

  • Health check: Liveness (JVM çalışıyor mu?) ve Readiness (istek kabul edebilir mi?) probe'larını doğru ayarlayın

  • Graceful shutdown: server.shutdown=graceful ile devam eden isteklerin tamamlanmasını bekleyin

  • JVM tuning: MaxRAMPercentage=75.0, UseG1GC, HeapDumpOnOutOfMemoryError — container ortamına uygun ayarlar

  • Database migration: Flyway/Liquibase kullanın, ddl-auto: validate — production'da asla otomatik şema değişikliği

  • Monitoring: Prometheus + Grafana ile metrikler, alert kuralları ile erken uyarı

  • Deployment stratejisi: Blue-green veya rolling deployment — her zaman rollback planı hazır