Ir al contenido

De Microservicios Simples a Spring Cloud: Una Migración Completa

·1671 palabras·8 mins
Michael Antonio Tomaylla
Autor
Michael Antonio Tomaylla
De la complejidad técnica a la simplicidad que genera valor

Introducción
#

En este post te mostraré cómo migrar una arquitectura de microservicios tradicional hacia una implementación con Spring Cloud. Partiremos de un sistema funcional con tres microservicios independientes (ms01producto, ms02reserva, ms03stock) y un frontend React, para evolucionarlo hacia una arquitectura más robusta y escalable utilizando los componentes de Spring Cloud.

📚 Lectura recomendada: Si quieres entender los conceptos teóricos de Spring Cloud antes de ver la implementación práctica, te recomiendo leer Spring Cloud: Claves para Sistemas Distribuidos Java donde explico los fundamentos y beneficios de esta tecnología.

📁 Repositorios del Proyecto
#

Para seguir este tutorial paso a paso, puedes descargar los siguientes repositorios:

🎯 Reto de Implementación: ¡Sería genial que uses el repositorio base como punto de partida y trates de implementar toda la migración a Spring Cloud por tu cuenta! Sigue los pasos de este tutorial como guía y luego compara tu resultado con el proyecto final. Es una excelente forma de consolidar el aprendizaje y desarrollar tus habilidades prácticas.

⚠️ Importante - Compatibilidad de Versiones:
Este tutorial utiliza Spring Cloud 2025.0.0 que es compatible con Spring Boot 3.5.x. Esta combinación asegura la máxima estabilidad y soporte para las últimas características. En todos los ejemplos de pom.xml encontrarás el dependencyManagement configurado correctamente para esta versión.

🏗️ Arquitectura Inicial: Antes de Spring Cloud
#

Nuestro punto de partida fue una arquitectura simple pero funcional:

Arquitectura Sin Spring Cloud

Características de la Arquitectura Original
#

  • Comunicación directa: Los servicios se comunicaban mediante URLs hardcodeadas
  • Configuración distribuida: Cada microservicio tenía su propia configuración
  • Sin descubrimiento de servicios: Las URLs se configuraban manualmente
  • Frontend directo: React se conectaba directamente a cada microservicio
  • Sin punto de entrada único: Múltiples puertos expuestos al cliente

Stack Tecnológico Original
#

# Microservicios existentes
ms01producto:  # Puerto 8081 - MongoDB
ms02reserva:  # Puerto 8080 - MySQL  
ms03stock:    # Puerto 8082 - MySQL
frontend:     # Puerto 3000 - React + Nginx

🚀 Arquitectura Final: Con Spring Cloud
#

La arquitectura evolucionada incluye todos los componentes de Spring Cloud:

Arquitectura Con Spring Cloud

Nuevos Componentes Agregados
#

  1. Config Server (Puerto 8888) - Configuración centralizada
  2. Service Discovery (Puerto 8761) - Eureka Server
  3. API Gateway (Puerto 8090) - Punto de entrada único

📋 Paso a Paso: Implementación de Spring Cloud
#

1. Config Server - Configuración Centralizada
#

Primero creamos el servidor de configuración centralizada.

Crear el proyecto ms-config
#

# Estructura del proyecto
ms-config/
├── src/main/java/com/indra/ms_config/
│   └── MsConfigApplication.java
├── src/main/resources/
│   └── application.properties
├── config-repo/
│   └── ms01producto.properties
└── pom.xml

Dependencias en pom.xml
#

<properties>
    <java.version>21</java.version>
    <spring-cloud.version>2025.0.0</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Clase Principal
#

package com.indra.ms_config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer  // ⭐ Habilita el Config Server
public class MsConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsConfigApplication.class, args);
    }
}

Configuración application.properties
#

spring.application.name=ms-config
server.port=8888

# Configuración para archivos locales
spring.profiles.active=native
spring.cloud.config.server.native.search-locations=file:///config-repo

# Actuator endpoints
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

Configuración centralizada para ms01producto
#

# config-repo/ms01producto.properties
spring.application.name=ms01producto
server.port=${SERVER_PORT:8081}

# MongoDB configuration
spring.data.mongodb.uri=${SPRING_DATA_MONGODB_URI:mongodb://localhost:27017/productos_db}

# Actuator configuration
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.info.env.enabled=true

2. Service Discovery - Eureka Server
#

Implementamos el servidor de descubrimiento de servicios.

Crear el proyecto ms-discovery
#

# Estructura del proyecto
ms-discovery/
├── src/main/java/com/indra/ms_discovery/
│   └── MsDiscoveryApplication.java
├── src/main/resources/
│   └── application.properties
└── pom.xml

Dependencias específicas
#

<properties>
    <java.version>21</java.version>
    <spring-cloud.version>2025.0.0</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Clase Principal
#

package com.indra.ms_discovery;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer  // ⭐ Habilita Eureka Server
public class MsDiscoveryApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsDiscoveryApplication.class, args);
    }
}

Configuración del Eureka Server
#

spring.application.name=ms-discovery
server.port=8761

# Configuración de Eureka Server
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.enable-self-preservation=false
eureka.client.service-url.defaultZone=${EUREKA_CLIENT_SERVICE_URL:http://localhost:8761/eureka/}

# Actuator
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

3. API Gateway - Punto de Entrada Único
#

Creamos el gateway para centralizar todas las peticiones.

Crear el proyecto ms-gateway
#

# Estructura del proyecto
ms-gateway/
├── src/main/java/com/indra/ms_gateway/
│   └── MsGatewayApplication.java
├── src/main/resources/
│   └── application.properties
└── pom.xml

Dependencias del Gateway
#

<properties>
    <java.version>21</java.version>
    <spring-cloud.version>2025.0.0</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Clase Principal (Simple)
#

package com.indra.ms_gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MsGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsGatewayApplication.class, args);
    }
}

Configuración de Rutas y CORS
#

spring.application.name=ms-gateway
server.port=8090

# Eureka client configuration
eureka.client.service-url.defaultZone=${EUREKA_CLIENT_SERVICE_URL:http://localhost:8761/eureka/}
eureka.instance.instance-id=${spring.application.name}:${spring.application.instance_id:${random.value}}

# Gateway Discovery Locator
spring.cloud.gateway.server.webflux.discovery.locator.enabled=true
spring.cloud.gateway.server.webflux.discovery.locator.lower-case-service-id=true

# CORS Configuration
spring.cloud.gateway.server.webflux.globalcors.add-to-simple-url-handler-mapping=true
spring.cloud.gateway.server.webflux.globalcors.cors-configurations.[/**].allowed-origins=http://localhost:3000
spring.cloud.gateway.server.webflux.globalcors.cors-configurations.[/**].allowed-methods=GET,POST,PUT,DELETE,OPTIONS,HEAD,PATCH
spring.cloud.gateway.server.webflux.globalcors.cors-configurations.[/**].allowed-headers=*

# Routes Configuration
spring.cloud.gateway.server.webflux.routes[0].id=ms01producto-routes
spring.cloud.gateway.server.webflux.routes[0].uri=lb://ms01producto
spring.cloud.gateway.server.webflux.routes[0].predicates[0]=Path=/ms01producto/**
spring.cloud.gateway.server.webflux.routes[0].filters[0]=StripPrefix=1

spring.cloud.gateway.server.webflux.routes[1].id=ms02reserva-routes
spring.cloud.gateway.server.webflux.routes[1].uri=lb://ms02reserva
spring.cloud.gateway.server.webflux.routes[1].predicates[0]=Path=/ms02reserva/**
spring.cloud.gateway.server.webflux.routes[1].filters[0]=StripPrefix=1

spring.cloud.gateway.server.webflux.routes[2].id=ms03stock-routes
spring.cloud.gateway.server.webflux.routes[2].uri=lb://ms03stock
spring.cloud.gateway.server.webflux.routes[2].predicates[0]=Path=/ms03stock/**
spring.cloud.gateway.server.webflux.routes[2].filters[0]=StripPrefix=1

4. Modificaciones a los Microservicios Existentes
#

Agregar dependencias Spring Cloud
#

En cada microservicio (ms01producto, ms02reserva, ms03stock):

<!-- Agregar al pom.xml -->
<properties>
    <java.version>21</java.version>
    <spring-cloud.version>2025.0.0</spring-cloud.version>
</properties>

<!-- Agregar estas dependencias -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<!-- Agregar el dependency management -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

💡 Nota sobre spring-cloud-starter-bootstrap:
Esta dependencia es obligatoria en Spring Boot 3.x para que funcione bootstrap.properties. Sin ella, Spring Cloud Config no podrá cargar la configuración antes del contexto de la aplicación, causando errores de conexión al Config Server.

Habilitar Discovery Client
#

// En ms01producto/src/main/java/com/indra/Ms01productoApplication.java
@SpringBootApplication
@EnableDiscoveryClient  // ⭐ Agregar esta anotación
public class Ms01productoApplication {
    public static void main(String[] args) {
        SpringApplication.run(Ms01productoApplication.class, args);
    }
}

Configurar bootstrap.properties
#

📝 Importante: En Spring Boot 3.x, para usar bootstrap.properties es necesario agregar la dependencia spring-cloud-starter-bootstrap que se incluye en las dependencias mostradas arriba. Sin esta dependencia, el archivo bootstrap.properties será ignorado.

# ms01producto/src/main/resources/bootstrap.properties
spring.application.name=ms01producto
spring.cloud.config.uri=${SPRING_CLOUD_CONFIG_URI:http://localhost:8888}

# Config Server retry configuration
spring.cloud.config.fail-fast=true
spring.cloud.config.retry.initial-interval=1500
spring.cloud.config.retry.max-interval=10000
spring.cloud.config.retry.max-attempts=10

# Eureka client configuration
eureka.client.service-url.defaultZone=${EUREKA_CLIENT_SERVICE_URL:http://localhost:8761/eureka/}
eureka.instance.instance-id=${spring.application.name}:${spring.application.instance_id:${random.value}}

Actualizar Comunicación Entre Servicios
#

// En ms02reserva - InventoryClientService
@Service
@RequiredArgsConstructor
public class InventoryClientService {
    
    private final RestTemplate restTemplate;
    private final DiscoveryClient discoveryClient;  // ⭐ Inyección de DiscoveryClient

    @Value("${stock.service.url:http://ms03stock:8082}")
    private String stockServiceUrl;

    public Boolean stockAvailable(String code) {
        // ⭐ Uso de Service Discovery
        ServiceInstance instance = discoveryClient.getInstances("MS03STOCK")
            .stream().findFirst().orElse(null);
        
        String url = instance.getUri() + "/api/stock/" + code;
        RestTemplate directRestTemplate = new RestTemplate();
        return directRestTemplate.getForObject(url, Boolean.class);
    }
}

5. Actualización del Docker Compose
#

Generación con docker init
#

Primero usamos docker init para generar los Dockerfiles base:

# Para cada microservicio
cd ms-config && docker init
cd ../ms-discovery && docker init  
cd ../ms-gateway && docker init
cd ../ms01producto && docker init
cd ../ms02reserva && docker init
cd ../ms03stock && docker init

Docker Compose Final
#

services:
  # Bases de datos (sin cambios)
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
    ports:
      - "3307:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./infra/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  mongo:
    image: mongo:6.0
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
      - ./infra/mongo/products.json:/data/products.json:ro
      - ./infra/mongo/mongo-init.sh:/docker-entrypoint-initdb.d/mongo-init.sh:ro
    command: ["bash", "/docker-entrypoint-initdb.d/mongo-init.sh"]

  # ⭐ Spring Cloud Components
  ms-config:
    build:
      context: ./ms-config
    environment:
      - SPRING_CLOUD_CONFIG_SERVER_NATIVE_SEARCH_LOCATIONS=file:///config-repo
    volumes:
      - ./ms-config/config-repo:/config-repo:ro
    ports:
      - 8888:8888

  ms-discovery:
    build:
      context: ./ms-discovery
    environment:
      - EUREKA_CLIENT_SERVICE_URL=http://ms-discovery:8761/eureka/
    ports:
      - 8761:8761

  ms-gateway:
    build:
      context: ./ms-gateway
    environment:
      - EUREKA_CLIENT_SERVICE_URL=http://ms-discovery:8761/eureka/
    ports:
      - 8090:8090
    depends_on:
      - ms-config
      - ms-discovery
      - ms01producto
      - ms02reserva
      - ms03stock

  # ⭐ Microservicios actualizados
  ms01producto:
    build:
      context: ./ms01producto
    environment:
      - SPRING_CLOUD_CONFIG_URI=http://ms-config:8888
      - EUREKA_CLIENT_SERVICE_URL=http://ms-discovery:8761/eureka/
      - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017/productos_db
    ports:
      - 8081:8081
    depends_on:
      - ms-config
      - ms-discovery
      - mongo

  ms02reserva:
    build:
      context: ./ms02reserva
    environment:
      - EUREKA_CLIENT_SERVICE_URL=http://ms-discovery:8761/eureka/
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/reservas_db?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=user
      - SPRING_DATASOURCE_PASSWORD=pass
      - STOCK_SERVICE_URL=http://ms03stock:8082
    ports:
      - 8080:8080
    depends_on:
      - ms-config
      - ms-discovery
      - mysql

  ms03stock:
    build:
      context: ./ms03stock
    environment:
      - EUREKA_CLIENT_SERVICE_URL=http://ms-discovery:8761/eureka/
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/stock_db?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=user
      - SPRING_DATASOURCE_PASSWORD=pass
    ports:
      - 8082:8082
    depends_on:
      - ms-config
      - ms-discovery
      - mysql

  # Frontend (sin cambios significativos)
  frontend:
    build:
      context: ./frontend
    ports:
      - 3000:80
    depends_on:
      - ms-gateway

volumes:
  mysql-data:
  mongo-data:

🔄 Comparación: Antes vs Después
#

Comunicación de Servicios
#

Antes:

// Frontend conectando directamente
const API_URLS = {
  productos: 'http://localhost:8081/api/productos',
  reservas: 'http://localhost:8080/api/reservas',
  stock: 'http://localhost:8082/api/stock'
};

Después:

// Frontend usando API Gateway
const API_BASE = 'http://localhost:8090';
const API_URLS = {
  productos: `${API_BASE}/ms01producto/api/productos`,
  reservas: `${API_BASE}/ms02reserva/api/reservas`,
  stock: `${API_BASE}/ms03stock/api/stock`
};

🚀 Comandos de Ejecución
#

Desarrollo Local
#

# 1. Iniciar bases de datos
docker compose up mysql mongo -d

# 2. Iniciar Spring Cloud components (en orden)
./mvnw spring-boot:run -f ms-config/pom.xml
./mvnw spring-boot:run -f ms-discovery/pom.xml  
./mvnw spring-boot:run -f ms-gateway/pom.xml

# 3. Iniciar microservicios
./mvnw spring-boot:run -f ms01producto/pom.xml
./mvnw spring-boot:run -f ms02reserva/pom.xml
./mvnw spring-boot:run -f ms03stock/pom.xml

# 4. Iniciar frontend
cd frontend && npm start

Producción con Docker
#

# Todo en un comando
docker compose up --build -d

# Verificar servicios
docker compose ps
docker compose logs ms-discovery  # Ver registro Eureka

📊 Beneficios Obtenidos
#

1. Configuración Centralizada
#

  • ✅ Un solo lugar para configurar todos los servicios
  • ✅ Actualizaciones en caliente sin reinicios
  • ✅ Configuraciones por ambiente (dev, prod)

2. Service Discovery
#

  • ✅ Auto-registro de servicios
  • ✅ Load balancing automático
  • ✅ Health checking integrado

3. API Gateway
#

  • ✅ Punto de entrada único para clientes
  • ✅ Enrutamiento dinámico basado en servicios
  • ✅ CORS centralizado
  • ✅ Posibilidad de agregar autenticación/autorización

4. Monitoreo y Observabilidad
#

  • ✅ Endpoints de Actuator en todos los servicios
  • ✅ Dashboard de Eureka para ver estado de servicios
  • ✅ Métricas centralizadas

🔍 URLs de Acceso
#

Interfaces de Administración
#

  • Eureka Dashboard: http://localhost:8761
  • Config Server: http://localhost:8888/ms01producto/default
  • Gateway Health: http://localhost:8090/actuator/health

APIs (via Gateway)
#

  • Productos: http://localhost:8090/ms01producto/api/productos
  • Reservas: http://localhost:8090/ms02reserva/api/reservas
  • Stock: http://localhost:8090/ms03stock/api/stock

Frontend
#

  • React App: http://localhost:3000

🎯 Próximos Pasos
#

Esta implementación sienta las bases para agregar:

  1. Circuit Breakers con Resilience4j
  2. Distributed Tracing con Sleuth/Zipkin
  3. Security con Spring Security OAuth2
  4. Metrics con Micrometer/Prometheus
  5. Caching distribuido con Redis

📝 Conclusión
#

La migración de una arquitectura de microservicios tradicional a Spring Cloud proporciona beneficios significativos en términos de:

  • Mantenabilidad: Configuración centralizada
  • Escalabilidad: Discovery automático y load balancing
  • Observabilidad: Monitoreo integrado
  • Resiliencia: Base para circuit breakers y retry policies

El proceso, aunque requiere cierta inversión inicial, proporciona una base sólida para el crecimiento y evolución del sistema a largo plazo.


Este post documenta la evolución completa de nuestro sistema Doggie Inn Pet Shop Management System hacia una arquitectura moderna con Spring Cloud.