Skip to content

ariaskeneth/spring-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Legacy Java Application Modernization - Scenario Results

Executive Summary

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.

1. Containerization

How would you structure the Dockerfile?

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

How would you handle external configurations?

Config Priority:

  1. ConfigMaps: For non-sensitive application configuration
  2. External Secrets Operator: For secure secret management with Vault/Secret Manager
  3. 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: password

How would you optimize the image for production?

Image Optimizations:

  1. Distroless Base Image: Reduces attack surface by 90%
  2. Layer Caching: Separate dependency and source code layers
  3. Multi-architecture: Support for linux/amd64 and linux/arm64
  4. Security Scanning: Integrated Trivy scanning in CI/CD
  5. Image Signing: If audit trails needed, container image signing can be implemented for complete supply chain security

Optimization Techniques:

  • Use .dockerignore to 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

2. Deployment on Kubernetes

Which kind of manifests would you create?

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:

  1. Deployment: Rolling updates for application pods with env contexts
  2. Service: Internal communication, observabilty and load balancing
  3. ConfigMap: Env variables and configuration data
  4. ExternalSecret: Vault integration for secrets
  5. HPA: Automatic horizontal scaling based on metrics
  6. PDB: High availability guaranteed during changes
  7. Ingress: External access with ingress controllers
  8. NetworkPolicy: Micro-segmentation and security
  9. Cluster: PostgreSQL Cluster with CloudNativePG operator
  10. ScheduledBackup: Automated backups to the ObjectStore

How would you manage scalability and high availability?

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-app

High 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.

How would you monitor the application's health?

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: 30

3. DevOps Practices

How would you set up a CI/CD pipeline?

Depending 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 results

Pipeline 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

How would you handle automated testing and versioning?

Testing Strategy:

  1. Security Tests: OWASP dependency scanning, container scanning
  2. Unit Tests: JUnit or JaCoCo tests
  3. Integration Tests: Testcontainers with real PostgreSQL
  4. 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

How would you implement rollback in case of failure?

Rollback Mechanisms:

  1. ArgoCD Rollback: Git revert to previous version, optional Argo Rollouts
  2. Kubernetes Rolling Update: Automatic rollback on health check failure
  3. Database Migrations: Backward-compatible schema changes
  4. Feature Flags: Runtime feature toggling

4. Observability & Logging

Which tools would you use for logging and monitoring?

Observability Stack (Existing Infrastructure Integration):

  1. Prometheus: Metrics collection with micrometer endpoint
  2. AlertManager: For alerting and notification
  3. Grafana: Visualization and dashboards
  4. Loki: Log aggregation and querying, logback endpoint
  5. Tempo: Distributed tracing, zipkin endpoint
  6. 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: critical

How would you ensure end-to-end visibility?

End-to-End Observability:

  1. Application Metrics: Custom business metrics via Micrometer/Prometheus
  2. Infrastructure Metrics: Kubernetes and container metrics
  3. Distributed Tracing: Tempo and optional integrations with OTel Infra
  4. Structured Logging and Tracing: JSON logs with standardized fields and IDs
  5. Error Tracking: Centralized error aggregation
  6. Performance Monitoring: APM with response time tracking

5. Security & Compliance

How would you securely manage secrets?

External Secret Management:

  1. External Secrets Operator: Integration with external Vault/Secret Manager
  2. Kubernetes Secrets: Runtime secret injection
  3. Secret Rotation: Automated secret rotation policies
  4. 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"

How would you implement access control on Kubernetes?

Access Control Implementation:

  1. RBAC: Role-based access control with least privilege
  2. Pod Security Standards: Enforce security policies
  3. Network Policies: Micro-segmentation
  4. 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: 8080

How would you ensure compliance?

Compliance Framework:

  1. Pod Security Standards: Enforce restricted security policies
  2. Audit Logging: Comprehensive Kubernetes API audit logs
  3. Vulnerability Scanning: Continuous container and dependency scanning
  4. Policy Enforcement: OPA Gatekeeper for policy as code
  5. 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: restricted

6. Data Persistence and Backups

How would you manage data persistence on Kubernetes?

Usually 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_basebackup and 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 instances number 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 monitoring stanza automatically creates a PodMonitor CRD, allowing Prometheus to scrape detailed PostgreSQL metrics without manual configuration.

How would you handle backups?

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: AES256

Backup 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

Architecture Overview

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
Loading

Key Benefits

  1. Security-First: Multi-layered security with zero-trust principles
  2. Cloud-Native: Kubernetes-native with modern GitOps practices
  3. Scalable: Auto-scaling with high availability
  4. Observable: Comprehensive monitoring and logging
  5. Compliant: Enterprise security and compliance standards
  6. Developer-Friendly: GitHub Flow with ephemeral PR environments
  7. Resource-Efficient: Resource optimization and efficient scaling

Conclusion

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.

About

Modernization of a Monolithic Spring Boot Application on Kubernetes

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published