This readme is my answer to the legacy Java Spring Boot application modernization scenario. This solution implements a cloud-native architecture using Kubernetes, following DevOps best practices with security-first principles and LGTM observability.
Multi-stage Dockerfile Structure:
# Stage 1: Build stage
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# Stage 2: Runtime stage using distroless image
FROM gcr.io/distroless/java21-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+ExitOnOutOfMemoryError -Xshare:on"
ENTRYPOINT ["java", "-jar", "app.jar"]Key Design Decisions:
- Multi-stage build: Separating build and runtime environments
- Eclipse Temurin JDK 21: the latest Adoptium JDK with Java LTS version for building
- Distroless runtime: the minimal attack surface, no shell or package manager
- Non-root user: Security best practice with least privilege
- Container-optimized JVM: configuration with proper memory and GC settings
Config Priority:
- ConfigMaps: For non-sensitive application configuration
- External Secrets Operator: For secure secret management with Vault/Secret Manager
- Environment Variables: For single container configuration
Configuration:
# ConfigMap for application configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
application.yml: |
server:
port: 8080
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,info
# External Secrets for Vault integration
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: postgres-secrets
spec:
secretStoreRef:
name: vault-secret-store
kind: SecretStore
target:
name: postgres-secrets
data:
- secretKey: postgres_user
remoteRef:
key: spring-app/postgres
property: username
- secretKey: postgres_password
remoteRef:
key: spring-app/postgres
property: passwordImage Optimizations:
- Distroless Base Image: Reduces attack surface by 90%
- Layer Caching: Separate dependency and source code layers
- Multi-architecture: Support for linux/amd64 and linux/arm64
- Security Scanning: Integrated Trivy scanning in CI/CD
- Image Signing: If audit trails needed, container image signing can be implemented for complete supply chain security
Optimization Techniques:
- Use
.dockerignoreto exclude unnecessary files - Minimize layers and divide or combine RUN commands where appropriate
- Use specific image tags with app versions, never
latest - Implement proper health checks via Kubernetes liveness/readiness probes
Kustomize-based Manifest Structure:
k8s/
├── base/ # Base manifests
│ ├── deployment.yaml # Application deployment
│ ├── service.yaml # App Service definition
│ ├── configmap.yaml # Configuration
│ ├── external-secrets.yaml # Secret management
│ ├── hpa.yaml # Horizontal Pod Autoscaler
│ ├── pdb.yaml # Pod Disruption Budget
│ ├── ingress.yaml # Ingress controller configuration
│ ├── rbac.yaml # RBAC configuration
│ └── network-policy.yaml # Network policies
├── database/base/ # PostgreSQL database manifests
│ ├── cluster.yaml # CloudNativePG Cluster
│ ├── scheduled-backup.yaml # Automated backups
│ └── object-store.yaml # ObjectStore configuration
└── overlays/ # Environment-specific
├── staging/ # Staging database and shared resources
├── production/ # Prod app and database
└── pr-template/ # Ephemeral PR environments
Key Manifests:
- Deployment: Rolling updates for application pods with env contexts
- Service: Internal communication, observabilty and load balancing
- ConfigMap: Env variables and configuration data
- ExternalSecret: Vault integration for secrets
- HPA: Automatic horizontal scaling based on metrics
- PDB: High availability guaranteed during changes
- Ingress: External access with ingress controllers
- NetworkPolicy: Micro-segmentation and security
- Cluster: PostgreSQL Cluster with CloudNativePG operator
- ScheduledBackup: Automated backups to the ObjectStore
Scalability Strategy:
- Horizontal Pod Autoscaling: Based on metrics and HPA configuration
- Pod Disruption Budget: High availability during deployments
# Prod Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: spring-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: spring-app
minReplicas: 5
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# Prod Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: spring-app-pdb
spec:
minAvailable: 3
selector:
matchLabels:
app: spring-appHigh Availability Features:
- Multi-replica deployment: Minimum 5 replicas in production
- Pod anti-affinity: Distribute application pods across physical nodes
- Rolling updates: Zero-downtime app deployments
- Health checks: Granular readiness, liveness, and startup probes.
- Database replicas: Managed by the CloudNativePG operator for automated failover, managed replicas, and backups.
- Multi-zone deployment: Distribute Kubernetes nodes across different availability zones or failure domains within the data center.
Monitoring Stack:
- Prometheus: Metrics collection from application pods
- Alertmanager: Alerting and notification for SLIs
- Grafana: Dashboards and visualization, centralizing observability
- Loki: Centralized log aggregation, logback endpoint
- Tempo: Distributed tracing after extensive configuration, zipkin endpoint
- Mimir: Long-term metrics storage
Health Probes Example:
containers:
- name: spring-app
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 40
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30Depending on the release and branching strategy, the project complexity, and the different git repositories, for the app, the k8s/argocd manifest files, and the infra with terraform modules. For the app I would usually use Github Actions for CI and ArgoCD for CD. I would set up the main app CI/CD pipeline like this:
GitHub Flow CI/CD Pipeline:
# GitHub Actions Pipeline Structure
name: Spring Boot CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan-and-lint:
name: Security Scan and Lint
steps:
- Secrets scan with TruffleHog
- Trivy filesystem scan
- Checkstyle/PMD with Maven validate
- Extract version from Maven project
build-and-sast:
name: Build and Static Analysis
needs: scan-and-lint
steps:
- Maven package and extract version
- OWASP Dependency Check (CVSS ≥9.0)
- CodeQL Analysis (security-extended queries)
- Security gates (0 critical, ≤5 high vulnerabilities)
pre-tests:
name: Unit and Integration Tests
needs: [scan-and-lint, build-and-sast]
steps:
- Unit tests with JUnit and JaCoCo
- Integration tests with Testcontainers (PostgreSQL 18)
- Self-hosted SonarQube scan with quality gates (after test execution)
- Quality gates (≥80% coverage requirement)
image-and-push:
name: Multi-Architecture Image Build and Push
needs: [scan-and-lint, build-and-sast, pre-tests]
steps:
- Multi-architecture image builds (AMD64/ARM64)
- Semantic versioning tags (GitHub Flow strategy)
- Container security scanning with Trivy
- GitHub Container Registry push
deploy:
name: Deploy to Environments
needs: [scan-and-lint, image-and-push]
steps:
- PR: Preview label for ArgoCD ApplicationSet PR Generator
- Main: Create PR to k8s/ArgoCD repository for GitOps
post-tests:
name: Post-Deployment Tests
needs: [deploy]
if: github.ref == 'refs/heads/main'
steps:
- Smoke tests (health, info, metrics endpoints)
- Load tests (performance baseline validation)
- End-to-end tests (critical user journeys)
- UAT tests (user acceptance criteria)
notify:
name: Pipeline Notification
needs: [scan-and-lint, build-and-sast, pre-tests, image-and-push, deploy, post-tests]
if: always()
steps:
- Comprehensive pipeline status summary
- PR comments with detailed resultsPipeline Features:
- GitHub Flow: Main branch + feature branches only
- Semantic Versioning: Extracted from Maven project version
- Security-First: Multiple security scanning layers
- Quality Gates: Coverage, security, and code quality thresholds
- GitOps: ArgoCD Repository with rolling deployments
Testing Strategy:
- Security Tests: OWASP dependency scanning, container scanning
- Unit Tests: JUnit or JaCoCo tests
- Integration Tests: Testcontainers with real PostgreSQL
- Quality Tests: CodeQL/self-hosted SonarQube with quality gate (80% coverage minimum)
Testcontainers Implementation:
@SpringBootTest
@Testcontainers
class ApplicationIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18")
.withReuse(true);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}Versioning Strategy:
- Source: Maven project version
- Main branch:
1.2.3 - Pull requests:
pr-123
Rollback Mechanisms:
- ArgoCD Rollback: Git revert to previous version, optional Argo Rollouts
- Kubernetes Rolling Update: Automatic rollback on health check failure
- Database Migrations: Backward-compatible schema changes
- Feature Flags: Runtime feature toggling
Observability Stack (Existing Infrastructure Integration):
- Prometheus: Metrics collection with micrometer endpoint
- AlertManager: For alerting and notification
- Grafana: Visualization and dashboards
- Loki: Log aggregation and querying, logback endpoint
- Tempo: Distributed tracing, zipkin endpoint
- Mimir: Long-term metrics storage
Integration Example:
# ServiceMonitor for Prometheus
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: spring-boot-app
spec:
selector:
matchLabels:
app: spring-boot-app
endpoints:
- port: http
path: /actuator/prometheus
interval: 10s
# PrometheusRule for alerting
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: spring-app-alerts
spec:
groups:
- name: spring-app.rules
rules:
- alert: ApplicationDown
expr: up{job="spring-boot-app"} == 0
for: 30s
labels:
severity: criticalEnd-to-End Observability:
- Application Metrics: Custom business metrics via Micrometer/Prometheus
- Infrastructure Metrics: Kubernetes and container metrics
- Distributed Tracing: Tempo and optional integrations with OTel Infra
- Structured Logging and Tracing: JSON logs with standardized fields and IDs
- Error Tracking: Centralized error aggregation
- Performance Monitoring: APM with response time tracking
External Secret Management:
- External Secrets Operator: Integration with external Vault/Secret Manager
- Kubernetes Secrets: Runtime secret injection
- Secret Rotation: Automated secret rotation policies
- Least Privilege: Minimal secret access permissions
Example:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-secret-store
spec:
provider:
vault:
server: "https://vault.domain.local"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "spring-app"
serviceAccountRef:
name: "spring-app"Access Control Implementation:
- RBAC: Role-based access control with least privilege
- Pod Security Standards: Enforce security policies
- Network Policies: Micro-segmentation
- Service Accounts: Application-specific identities
RBAC Configuration:
# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: spring-app
namespace: spring-app
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: spring-app-role
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
# NetworkPolicy for micro-segmentation
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: spring-app-netpol
spec:
podSelector:
matchLabels:
app: spring-app
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
app: ingress-controller
ports:
- protocol: TCP
port: 8080Compliance Framework:
- Pod Security Standards: Enforce restricted security policies
- Audit Logging: Comprehensive Kubernetes API audit logs
- Vulnerability Scanning: Continuous container and dependency scanning
- Policy Enforcement: OPA Gatekeeper for policy as code
- Compliance Reporting: Automated compliance dashboards
Security Policies:
# Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: spring-app
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedUsually with StatefulSets. Although managing stateful workloads like databases directly with StatefulSets is practical but introduces excessive manual overhead for continuous maintenance activities such as high availability, backups, monitoring, and version upgrades. A more mature and easy approach is to leverage a specialized Kubernetes Operator, to automate this database lifecycle. For this solution, I suggest CloudNativePG, a leading open-source operator specifically designed for PostgreSQL.
PostgreSQL Persistence Strategy with CloudNativePG: Instead of manually defining StatefulSets, Services, and backup CronJobs, we define declarative custom resources: Cluister, ScheduledBackup and ObjectStore. The operator then reconciles the state of the cluster to match this definition.
# PostgreSQL Cluster managed by CloudNativePG Operator
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-app-cluster
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:18.0-standard-trixie
monitoring:
enablePodMonitor: true
storage:
size: 100Gi
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: s3local-eu-central
Key Benefits of the Operator-Led Approach:
- Automated Failover: The operator continuously monitors the primary instance. If it fails, a replica is automatically and safely promoted to primary, typically within seconds.
- Simplified Backups & PITR: Backup and Point-in-Time-Recovery (PITR) are configured declaratively. The operator manages
pg_basebackupand WAL archiving to an S3-compatible object store (like MinIO or Rook Ceph), eliminating the need for custom CronJobs. - Managed Read Replicas: Scaling read capacity is as simple as changing the
instancesnumber in the manifest. The operator handles the creation and synchronization of new streaming replicas. - Zero-Downtime Upgrades: The operator can perform rolling updates to upgrade PostgreSQL minor versions with zero downtime.
- Integrated Monitoring: The
monitoringstanza automatically creates aPodMonitorCRD, allowing Prometheus to scrape detailed PostgreSQL metrics without manual configuration.
Backup Strategy:
---
# Scheduled Backup with CloudNativePG Operator
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: postgres-app-cluster-backup
spec:
cluster:
name: postgres-app-cluster
schedule: '1 2 3 * * *'
backupOwnerReference: self
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
---
# ObjectStore for backups
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: s3local-eu-central
spec:
retentionPolicy: "7d"
configuration:
destinationPath: "s3://on-prem-s3-bucket/PGbackups/"
endpointURL: "http://minio.storage.central.eu.local:9000" # Or Ceph S3 endpoint
s3Credentials:
accessKeyId:
name: s3local-eu-central
key: ${ACCESS_KEY_ID}
secretAccessKey:
name: s3local-eu-central
key: ${ACCESS_SECRET_KEY}
wal:
compression: gzip
encryption: AES256Backup Features:
- Daily Automated Backups: Scheduled to the s3local service
- 7-Day Retention: Automatic cleanup of old backups
- Declarative Restore: Restore from backup to new cluster
- S3-Compliant: MinIO or Rook Ceph S3-compatible object store
graph LR
subgraph "External Infrastructure"
VAULT[Vault / Secret Manager]
end
subgraph "GitHub CI"
GIT[App Repository] --> GHA[GitHub Actions]
GHA --> REG[Container Registry]
GHA --> CLUS[Cluster Repository]
end
subgraph K8S[Kubernetes Cluster]
subgraph AC[App Config]
subgraph "Application Layer"
APP[Spring Boot App]
HPA[Auto Scaler]
end
subgraph "Data Layer"
PG[CloudNativePG Cluster]
PG --> BAK[Backup]
BAK --> OBJ[ObjectStore]
end
subgraph "Security"
RBAC[Role-Based Access Control]
ES[External Secrets]
NP[Network Policies]
INC[Ingress Config]
end
end
subgraph "Observability"
PROM[Prometheus]
LOKI[Loki]
TEMPO[Tempo]
LOKI --> GRAF
TEMPO --> GRAF
PROM --> GRAF[Grafana]
PROM --> MIM[Mimir]
end
subgraph "Operators"
ARGOCD[ArgoCD]
ESO[External Secrets Operator]
CNPG[CloudNativePG Operator]
end
end
CLUS --> ARGOCD
REG --> ARGOCD
ARGOCD --> AC
APP --> PG
LOKI --> APP
PROM --> APP
TEMPO --> APP
INC --> APP
HPA --> APP
VAULT --> ESO
ESO --> ES
ES --> APP
CNPG --> PG
- Security-First: Multi-layered security with zero-trust principles
- Cloud-Native: Kubernetes-native with modern GitOps practices
- Scalable: Auto-scaling with high availability
- Observable: Comprehensive monitoring and logging
- Compliant: Enterprise security and compliance standards
- Developer-Friendly: GitHub Flow with ephemeral PR environments
- Resource-Efficient: Resource optimization and efficient scaling
This modernization approach transforms the legacy Spring Boot application into a cloud-native, secure, and scalable solution. The implementation follows industry best practices for containerization, Kubernetes deployment, DevOps automation, observability, and security compliance. The solution provides a robust foundation for future growth while maintaining high availability and developer productivity.
The comprehensive documentation, example configurations, and implementation guides ensure successful deployment and operation of the modernized application in production environments.