Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

La configuración de una aplicación es indispensable para su funcionamiento, permite no hardcoredar ciertos valores en el código fuente al mismo tiempo que externalizarlos en archivos de más fácil edición. Externalizar la configuración de la aplicación permite utilizar el mismo artefacto binario en todos los entornos, los valores que cambian en cada entorno es posible proporcionarlos de diferentes formas y formatos desde archivos en el classpath hasta variables de entorno o un servidor de configuración. Spring Boot permite obtener los valores de diferentes fuentes e implementa un mecanismo de prioridad para determinar el valor a usar.

Spring

Java

La configuración de Spring Boot proporciona un mecanismo muy flexible para la configuración de diferentes fuentes. Cada fuente tiene un orden de preferencia para establecer los valores de las propiedades, además se integra con el servidor de configuración centralizada de Spring Cloud Config.

La configuración permite cambiar el comportamiento de la aplicación sin cambiar el código ni generar un nuevo artefacto. No hardcodear los valores en el código y extraer la configuración permite utilizar el mismo artefacto en cualquier entorno, ya sea desarrollo, pruebas o producción. Utilizar el mismo artefacto para todos los entornos tiene la ventaja de no introducir un error en la construcción del artefacto como cabría la posibilidad generando un artefacto binario para cada uno de los entornos, usar el mismo artefacto es necesario para que las pruebas realizadas sobre el artefacto en el entorno de desarrollo o pruebas se consideren válidas para producción.

Aunque en algunos sitios se recomienda que la configuración de la aplicación esté separada de artefacto de despliegue, en realidad más que la configuración esté separada es necesario poder tener un mecanismo de orden de preferencia de los valores de configuración. La aplicación puede tener unos valores de configuración por defecto pero es necesario poder sobreescribirlos debido a que algunos no se desean incluir en el código fuente o en el propio artefacto, también es necesario para cambiar los valores por defecto incluidos en el artefacto si se desea corregir un error sin necesidad de generar un nuevo artefacto.

Por otro lado es aconsejable tener bajo el control de versiones los archivos de configuración como cualquier otro archivo de código fuente de la aplicación, para con el historial del control de versiones ver los cambios que se han hecho o volver a versiones anteriores.

Necesidades según el rol

Las diferentes personas cada una con su rol desea tener la capacidad de configurar la aplicación. A los desarrolladores nos interesa para poder externalizar ciertas variables del código de la aplicación para tener la capacidad de cambiar los valores sin modificar el código. Esto es más fácil que encontrar donde están los valores hardcodeados y modificar las diferentes coincidencias, y evitando tener que recompilar.

Aunque los archivos de configuración no son código ejecutable forman parte del código de la aplicación si la configuración se incluye dentro del artefacto, también por comodidad el desarrollador desea cambiarlos al mismo tiempo que el código para mantener la consistencia entre el código y la configuración, ya que todas la variables de configuración que requiere el código deben tener un valor sino se produce un error en tiempo de ejecución.

Las personas con el rol de sistemas o SRE y por las tareas de mantenimiento de sistemas y operaciones también requieren tener la capacidad de cambiar las propiedades ajustando los valores a los adecuados según el entorno de ejecución sin modificar el artefacto, quizá no sobrescribir los valores de todas las propiedades pero si las relevantes desde el punto de vista de sistemas.

Es necesario para ajustar los valores por defecto o hacer una corrección que no requiera generar un nuevo artefacto sino simplemente ajustar un valor de configuración. Por rapidez y porque hacer una corrección generando un nuevo artefacto requiere pasar todo el proceso de pruebas para asegurar que el nuevo artefacto no incluye algún cambio adicional no deseado.

También por motivos de seguridad es necesario externalizar los valores de algunas variables como contraseñas, claves y certificados, de modo que aunque alguien tenga acceso al artefacto no tenga acceso a las credenciales de los servicios que usa.

Los valores adicionales se proporcionan habitualmente como variables de entorno o con archivos externalizados del artefacto que se buscan en el sistema de archivos, de esta forma la configuración incluida en el artefacto por los desarrolladores es sobrescrita por la configuración por la proporcionada por las personas con el rol de sistemas.

La solución para estas diferentes necesidades de los diferentes roles es obtener los valores de las variables de configuración de varias fuentes junto un de orden de preferencia para determinar que valor se toma en caso de que esté definido en varias fuentes.

En las configuraciones más avanzadas es necesario un mecanismo para que las aplicaciones obtengan la configuración de un servidor donde esté centralizada. Al igual que un servicio de registro y descubrimiento es esencial para los microservicios un servicio de obtención de configuración de donde obtengan su configuración es también útil. Dado el gran número de microservicios de los que puede estar compuesto un sistema, su carácter efímero, los varios entornos de ejecución (desarrollo, pruebas, producción, …) mantener centralizada la configuración en un único sitio hace las cosas mucho más sencillas cuando hay que cambiar el valor de alguna propiedad. En vez de las alternativas con un archivo de configuración, aún externalizado del artefacto, en el sistema de archivos del entorno de ejecución o a través de variables de entorno que deben ser aprovisionadas.

Configuración en una aplicación de Spring Boot

Spring Boot integra la funcionalidad de obtener la configuración de varias fuente y define un orden de preferencias en caso de conflicto. Spring Cloud Config Server es un servicio que proporciona un mecanismo adicional para centralizar la configuración de las aplicaciones.

En una aplicación monolítica, un monolito modular o en un entorno donde no hay muchas aplicaciones el mecanismo de configuración proporcionado Spring Boot es suficiente. Sin embargo, en un entorno de microservicios o donde hay muchas aplicaciones tener una configuración centralizada proporciona varios beneficios. Los beneficios de un servidor de configuración es centralizar en un única fuente lo que facilita su ubicación, modificación y despliegue en las aplicaciones.

Orden de preferencia de las propiedades

Spring Boot soporta varias fuentes de las que obtener la configuración desde archivos de configuración en el classpath, archivos externalizados en el sistemas de archivos, argumentos del programa, propiedades del sistema de la máquina virtual, variables de entorno e incluso otros mecanismos extensibles personalizados.

Cada una de estas fuentes tienen un orden de búsqueda y prioridad donde las fuentes posteriores sobrescriben los valores de las anteriores o se añaden nuevas variables.

En la documentación de Spring Boot están detalladas estas fuentes y prioridad entre ellas. Por ejemplo, la configuración establecida en los archivos de configuración es sobrescrita por la configuración proporcionada como variables de entorno.

Con Spring Cloud Config las propiedades del servidor de configuración se cargan con posterioridad de los archivos de datos de configuración incluidos en el classpath dentro del artefacto o de los archivos externalizados en el sistema de archivos. Sin embargo, la configuración establecida como variables de entorno siguen teniendo más preferencia.

Archivos de datos de configuración

Los archivos de configuración entre ellos también tienen un orden de búsqueda en varios directorios y prioridad, iguamente detallado en la documentación de Spring Boot. Teniendo más preferencia los archivos externalizados y dentro de estos los más específicos para un entorno de ejecución.

Las ubicaciones en las que Spring Boot archivos de configuración también tiene una preferencia además de ser a su vez configurable.

Los archivos de configuración se pueden definir en el formato properties y yaml. La ventaja del formato yaml es que permite agrupar las propiedades de forma jerárquica que es más legible que el formato properties habitualmente utilizado en las aplicaciones Java por defecto. La desventaja de yaml es que es un formato en el que una mala tabulación genera algún tipo de error o mal funcionamiento.

1
2
app.properties.classpath=classpath
app.properties.external=
application-format.properties
1
2
3
4
app:
  properties:
    classpath: classpath
    external: 
application-format.yml

Propiedades que afectan a la configuración

En el sistema de configuración de Spring hay ciertas variables que afectan y permiten adaptar la configuración por defecto a las preferencias o necesidades de la aplicación.

Algunas de estas propiedades son el nombre del servicio, los perfiles activos o las ubicaciones de búsqueda de archivos de configuración.

1
2
3
4
5
6
7
spring:
  application:
    name: service
  profiles:
    active: production
  config:
    additional-location: optional:classpath:/custom-config/,optional:file:./custom-config/
spring-boot-config-properties.yml

Estas otras propiedades se utilizan cuando la aplicación de Spring Boot obtiene la configuración adicionalmente de un servidor de configuración de Spring Cloud Config.

1
2
3
4
5
6
7
8
spring:
  config:
    import: optional:configserver:http://localhost:8090
  cloud:
    config:
      name: service
      profile: production
      label: 2.0
spring-boot-cloud-config-properties.yml

En las rutas de búsqueda con el prefijo optional: en caso de no encontrarse la fuente el inicio de la aplicación en vez de fallar con una excepción se ignora y se continúa a riesgo de utilizar los valores de las fuentes anteriores e ignorando lo que tuviese esa fuente opcional.

1
2
3
4
5
6
7
8
spring:
  cloud:
    config:
      location:
        optional: ./config
spring:
  config:
    import: optional:configserver:http://localhost:8090
spring-boot-optional.yml

El servidor de configuración centralizada Spring Cloud Config Server

Un servidor de configuración permite cambiar o proporcionar una forma adicional de la que la aplicación obtiene propiedades y valores de configuración. La aplicación al iniciar realiza una petición al servidor de configuración y obtiene las propiedades adicionales de configuración. En el caso de Spring Cloud Config Server ofrece una interfaz REST que usa las aplicaciones para realizar la petición.

Por otro lado, la forma de configurar la aplicación cambia, en vez de proporcionar la configuración a la aplicación a cada una de las instancias de su servicio en variables de entorno o en archivos estáticos es cada instancia de la aplicación la que obtiene la configuración de un servidor. Es muy interesante para las personas con el rol de operaciones o SRE y para la arquitectura del sistema.

Otra de sus utilidades es una forma de que ciertos servicios obtengan la configuración cuando sus entornos y sistemas de archivos son efímeros como es el caso de las funciones de Google Cloud o lambdas de AWS.

Dado que este servicio de configuración es esencial para que los microservicios puedan obtener su configuración sin el cual no pueden proporcionar su funcionalidad hay que configurarlo de tal manera que sea tolerante a fallos. Una de las medidas para hacerlo tolerante a fallos es iniciar varias instancias de servidores de configuración, estas instancias se autorregistran en el servicio de descubrimiento para que los microservicios puedan descubrirlos y obtener su configuración al iniciarse.

Fuentes de configuración

El servidor de configuración centralizada Spring Cloud Config soporta varios sistemas diferentes en los que almacenar las propiedades de configuración o backends para recuperarlos cuando una instancia del servicio la solicite.

Una opción es utilizar un repositorio de Git con las ventajas asociadas del control de versiones como historial para mantener un registro de los cambios o volver a una versión anterior. Otros son un sistema de archivos, en una base de datos relacional con JDBC, Redis, Vault y algunos otros específicos más.

Propiedades que afectan a la configuración de Spring Cloud Config Server

El servidor de configuración de Spring Cloud Config también tiene variables de configuración, varias según el sistema de almacenamiento o backend donde se persisten las propiedades de configuración de los servicios. Otras propiedades son para proporcionar las credenciales de autenticación de los backends.

En el caso de Git la propiedad label del servicio en Git puede ser la huella o hash del commit, una rama o una etiqueta.

1
2
3
4
5
6
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/picodotdev/configuration-repository
spring-cloud-config-server-git-properties.yml

En el caso del sistema de archivos como backend esta propiedad permite configurar las rutas en las que buscar los archivos de configuración y la disposición de los archivos de configuración en la estructura de directorios. En este caso la propiedad label es la versión de la aplicación.

Las propiedades application, profile y label permiten identificar la configuración de un servicio, para un entorno y de una versión específica.

1
2
3
4
5
6
7
8
spring:
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          searchLocations: file:./misc/config/,file:./misc/config/{application}/,file:./misc/config/{application}/{profile}/,file:./misc/config/{application}/{profile}/{label}
spring-cloud-config-server-filesystem-properties.yml

Ejemplo de configuración en aplicación de Spring Boot

Esta aplicación de Spring Boot tiene varias propiedades de configuración. Para mostrar el mecanismo de preferencia en la resolución de los valores cada una de las propiedades se obtiene de una fuente distinta. En esta lista de menor preferencia a mayor preferencia, desde un archivo de configuración en el classpath, archivo externalizado, servidor de configuración, argumento de programa y variable de entorno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.springcloudconfig.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main implements CommandLineRunner {

    @Value("${app.properties.classpath}")
    private String classpath;

    @Value("${app.properties.external}")
    private String external;

    @Value("${app.properties.argument}")
    private String argument;

    @Value("${app.properties.environment}")
    private String environment;

    @Value("${app.properties.cloud}")
    private String cloud;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("Application classpath property: " + classpath);
        System.out.println("Application external property: " + external);
        System.out.println("Application argument property: " + argument);
        System.out.println("Application environment property: " + environment);
        System.out.println("Application cloud property: " + cloud);
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main-client.java

Este es el archivo de configuración que se incluye en el classpath y como parte del artefacto, no está externalizado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
spring:
  application:
    name: service
  profiles:
    active: production
  config:
    import: optional:configserver:http://localhost:8090
    failFast: true
  cloud:
    config:
      label: 2.0

app:
  properties:
    classpath: "classpath"
    external: ""
    argument: ""
    environment: ""
    cloud: ""
application-classpath.yml

Este archivo de configuración externo al artefacto proporciona el valor de una propiedad.

1
2
3
app:
  properties:
    external: "external"
application-external.yml

Sin ninguna configuración adicional y con el servidor de configuración no iniciado estos son lo valores que toman las propiedades en la aplicación.

1
2
$ ./gradlew service:run

gradle-run-1.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: 
Application environment property: 
Application cloud property:
System.out-1

Añadiendo al iniciar el programa un argumento o variable de entorno para configurar el valor de una propiedad la aplicación toma el valor proporcionado.

1
2
$ ./gradlew service:run --args="--app.properties.argument=argument"

gradle-run-2.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: argument
Application environment property: 
Application cloud property:
System.out-2
1
2
$ APP_PROPERTIES_ENVIRONMENT="enviroment" ./gradlew service:run

gradle-run-3.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: 
Application environment property: enviroment
Application cloud property:
System.out-3

Con el servidor de configuración iniciado la aplicación en este caso adicionalmente toma el valor de la configuración para la aplicación del servidor. En casos casos anteriores la aplicación en el inicio no falla porque la fuente del servidor de Spring Cloud Config Server se considera opcional.

1
2
$ ./gradlew configserver:run

gradle-run-4.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: 
Application environment property: 
Application cloud property: cloud-default
System.out-4

Cambiando la propiedad label o como variable de entorno a través de los argumentos en el inicio del servicio es posible cambiar la versión que el servidor de configuración devuelve para el servicio.

1
2
$ SPRING_CLOUD_CONFIG_LABEL="1.0" ./gradlew service:run

gradle-run-5.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: 
Application environment property: 
Application cloud property: cloud-1.0
System.out-5
1
2
$ SPRING_CLOUD_CONFIG_LABEL="2.0" ./gradlew service:run

gradle-run-6.sh
1
2
3
4
5
Application classpath property: classpath
Application external property: external
Application argument property: 
Application environment property: 
Application cloud property: cloud-2.0
System.out-6

Las dependencias en el archivo de construcción con Gradle son las siguientes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform('org.springframework.boot:spring-boot-dependencies:2.6.1'))
    implementation(platform('org.springframework.cloud:spring-cloud-dependencies:2021.0.0'))

    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    implementation('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    implementation('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    implementation('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)

    runtimeOnly('com.google.code.gson:gson:2.8.9')
    runtimeOnly('com.fasterxml.jackson.core:jackson-databind:2.13.0')
    runtimeOnly('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.0')
}

application {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
    mainClass = 'io.github.picodotdev.blogbitix.springcloudconfig.service.Main'
}

build-client.gradle

Ejemplo de configuración centralizada con Spring Cloud Config Server

El servidor de configuración de Spring Cloud Config es posible implementarlo como una aplicación de Spring Boot. La aplicación de Spring Boot simplemente requiere utilizar la anotación @EnableConfigServer y configurar el almacenamiento del backend para las propiedades de configuración, en el ejemplo utilizando el sistema de archivos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springcloudconfig.configserver;

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

@SpringBootApplication
@EnableConfigServer
public class Main {

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main-server.java

Los archivos de configuración para los microservicios en este ejemplo están en el directorio misc/config donde siguiendo algunas convenciones para asignar el nombre a los archivos se pueden personalizar las configuraciones de los microservicios según el entorno y perfil con el que se active. Spring Cloud Config denomina un backend como el sistema de almacenamiento de los datos de configuración en este caso se utiliza el sistema de archivos, sin embargo, hay otras disponibles como un repositorio de git el cual ofrece varias ventajas propias de un repositorio de código como historial, ramas de trabajo y hacer cambios con un commit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
server:
  port: ${port:8090}

spring:
  profiles:
    active: native
  application:
    name: configserver
  cloud:
    config:
      server:
        native:
          searchLocations: file:./misc/config/,file:./misc/config/{application}/,file:./misc/config/{application}/{profile}/,file:./misc/config/{application}/{profile}/{label}

application-server.yml

Con los siguientes archivos de configuración en el servidor para el servicio, en función de la versión de la aplicación solicitada las propiedades devueltas cambian. Estos comandos solicitan al servidor la configuración de la aplicación a través de una petición de red con la interfaz REST, lo datos se devuelven en formato JSON.

1
2
3
app:
  properties:
    cloud: "cloud-1.0"
application-1.0.yml
1
2
3
app:
  properties:
    cloud: "cloud-2.0"
application-2.0.yml
1
2
3
app:
  properties:
    cloud: "cloud-default"
application-default.yml
1
2
$ curl -v "http://localhost:8090/service/production/1.0"

curl-spring-cloud-config-server-1.0.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "name": "service",
  "profiles": [
    "production"
  ],
  "label": "1.0",
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "file:misc/config/service/production/1.0/application.yml",
      "source": {
        "app.properties.cloud": "cloud-1.0"
      }
    },
    {
      "name": "file:misc/config/service/application.yml",
      "source": {
        "app.properties.cloud": "cloud-default"
      }
    }
  ]
}
curl-spring-cloud-config-server-1.0.out
1
2
$ curl -v "http://localhost:8090/service/production/2.0"

curl-spring-cloud-config-server-2.0.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "name": "service",
  "profiles": [
    "production"
  ],
  "label": "2.0",
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "file:misc/config/service/production/2.0/application.yml",
      "source": {
        "app.properties.cloud": "cloud-2.0"
      }
    },
    {
      "name": "file:misc/config/service/application.yml",
      "source": {
        "app.properties.cloud": "cloud-default"
      }
    }
  ]
}
curl-spring-cloud-config-server-2.0.out
1
2
$ curl -v "http://localhost:8090/service/production/"

curl-spring-cloud-config-server-default.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "name": "service",
  "profiles": [
    "production"
  ],
  "label": null,
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "file:misc/config/service/application.yml",
      "source": {
        "app.properties.cloud": "cloud-default"
      }
    }
  ]
}
curl-spring-cloud-config-server-default.out

Dado que el servicio de configuración se convierte en crítico para el inicio de las aplicaciones es recomendable tener varias instancias del mismo para proporcionar tolerancia a fallos. Y en una arquitectura de microservicios quizá utilizando registro y descubrimiento de servicios.

Otra necesidad es cifrar algunas propiedades, para ello el servidor de configuración también proporciona dos endpoints uno para hacer el cifrado y otro para hacer el descifrado.

Incluso es posible recargar la configuración de una aplicación de Spring Boot sin reiniciarla.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew service:run


Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  9. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  10. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  11. Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon
  12. Proxy para microservicios con Spring Cloud Netflix y Zuul
  13. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  14. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  15. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  16. Trazabilidad en microservicios con Spring Cloud Sleuth
  17. Implementar tolerancia a fallos con Resilience4j
  18. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  19. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  20. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  21. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
  22. Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config
Comparte el artículo: