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: fullCustom 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 timeoutLiveness 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: 30sGraceful shutdown süreci:
Shutdown sinyali alınır (SIGTERM)
Yeni istek kabul edilmesi durdurulur
Devam eden isteklerin tamamlanması beklenir (30 saniye timeout)
Tamamlanamayanlar 30 saniye sonra zorla kesilir
Spring context kapatılır (bean'ler destroy edilir)
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
terminationGracePeriodSecondsdeğerini Spring Boot'untimeout-per-shutdown-phasedeğ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ı engellerFarklı 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: trueMigration 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ı sorun5. 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: warning7. 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 -dRollback 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 myapp10. 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=trueTam 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=gracefulile devam eden isteklerin tamamlanmasını bekleyinJVM tuning:
MaxRAMPercentage=75.0,UseG1GC,HeapDumpOnOutOfMemoryError— container ortamına uygun ayarlarDatabase migration: Flyway/Liquibase kullanın,
ddl-auto: validate— production'da asla otomatik şema değişikliğiMonitoring: Prometheus + Grafana ile metrikler, alert kuralları ile erken uyarı
Deployment stratejisi: Blue-green veya rolling deployment — her zaman rollback planı hazır
AI Asistan
Sorularını yanıtlamaya hazır