Spring Cloud: How To Implement Service Discovery (Part 2)
In this article, explore two different architectural approaches to combine Service Discovery and Remote Configuration.
Join the DZone community and get the full member experience.
Join For FreeIn the previous part of this article, Spring Cloud: How to Implement Service Discovery (Part 1), we saw the basics of Service Discovery in the context of Spring Cloud. We have seen that the Netflix OSS Eureka component is still the main choice. In this post, we are going to discuss some Eureka additional topics, such as:
- Java client API
- REST API
- Secure the discovery server and the client services
- Combine Service Discovery with Distributed Configuration
Service Discovery: Client Java API
In the examples in the first part of this article, the registering and fetching features were running under the hood and we have only seen the results of testing the whole architecture by calling a client REST endpoint. There is also a way to interact with the Eureka API programmatically, by using Java method calls. A possible choice would be to use the EurekaClient
class. For example, if we want to get all the instances of a service identified by a particular ID, we could write the following code, supposing we have a client implemented as a Spring Boot application exposing REST services:
@Autowired
private EurekaClient eurekaClient;
@GetMapping("/testEurekaClient")
public String testEurekaClient() {
Application application = eurekaClient.getApplication("CLIENT-SERVICE");
List<InstanceInfo> instanceInfos = application.getInstances();
if (instanceInfos != null && instanceInfos.size() > 0 ) {
return instanceInfos.get(0).getHomePageUrl();
}
return null;
}
Spring Cloud offers an alternative to the above with its own DiscoveryClient
class. So, we can implement the above code also with the following:
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/testDiscoveryClient")
public String testDiscoveryClient() {
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("CLIENT-SERVICE");
if (serviceInstances != null && serviceInstances.size() > 0 ) {
return serviceInstances.get(0).getServiceId();
}
return null;
}
Service Discovery: Eureka's REST API
We have seen in the previous section how we can use a Java API to interact with the Eureka discovery server. In a more general scenario, we might have components not written in Java. In that case, we can use a REST API provided by Eureka. Some of its endpoints are described hereafter:
- /eureka/v2/apps: With GET operation, gets all the instances
- /eureka/v2/apps/appID: If used with a POST action, it registers an application instance, and with a GET, it fetches all the application IDs.
- /eureka/v2/apps/appID/instanceID: If used with a DELETE action, it removes the specific instance from the server's registry, whereas a PUT sends a "heartbeat" to the server for that particular instance.
- /eureka/v2/apps/appID/instanceID/metadata?key=value: With a PUT serves the purpose of updating metadata
Service Discovery: Securing the Server
Putting in place a minimum level of security is important for discovery instances in a production environment. We can protect the discovery server, for instance, by a simple authentication phase based on username and password. To do so, we should set a username and password in the application.yml file by the spring.security.user properties:
spring:
security:
user:
name: myusername
password: mypassword
Then, in order to be able to authenticate and communicate with the service discovery server we must set something like the following on the client-side configuration, supposing the server port is 8760 and is running locally along with the client:
eureka:
client:
serviceUrl:
defaultZone: http://myusername:mypassword@localhost:8760/eureka/
Service Discovery: Securing the Client
The client side in this discussion is made by the service components of our microservice system. Securing them means securing the communication channels for each component. If the clients expose its features through REST services, the communication channel lies on HTTP protocol. We can secure it by enforcing SSL communication.
To enable SSL on the client side, we need a certificate in the first place. We can generate a self-signed certificate using the keytool
Java program, like this:
keytool -genkey -alias client -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 365
Then we can take the generated keystore.p12
file and copy it into the /resources folder of our Spring Boot application and set the following piece of configuration in the application.yml file:
server:
ssl:
key-store: classpath:keystore.p12
key-store-password: mypassword
keyStoreType: PKCS12
keyAlias: client
By the above, we can call our service using HTTPS in the protocol part of the URL. We can improve the configuration by enabling HTTPS and not HTTP, by setting the following Eureka instance properties:
eureka:
instance:
securePortEnabled: true
nonSecurePortEnabled: false
statusPageUrl: https://${eureka.hostname}:${server.port}/info
healthCheckUrl: https://${eureka.hostname}:${server.port}/health
homePageUrl: https://${eureka.hostname}:${server.port}/
Here we have also set some endpoints, like the info, health, and the home page URL.
Service Discovery: Combine With Remote Configuration
We can combine service discovery with distributed configuration in two different ways:
- Config First approach
- Discovery First approach
We are going to describe these two approaches in the following sections.
Config First
In the Config First approach, the remote configuration server is the central point of the architecture. Both client and discovery service instances will connect to the config server in order to fetch their configuration.
To set up the configuration server, we have to add this dependency to the Maven POM file:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
In the application.yml file, to simplify the example, we are going to set the spring.profiles.active property as native
. This way we can use a local repository, based on configuration files stored in the classpath, and we can choose to put them in a /config subfolder of the /resources Spring Boot application directory. We also secure the server with basic authentication by the security.user property.
spring:
profiles:
active: native
application:
name: config-server
security:
user:
name: myusername
password: mypassword
In the application.yml file of the client and discovery service applications, we will store just the basic properties; for instance, those required to connect to the configuration server. The rest of the properties will be stored on the configuration server itself, in specific files stored in the /config folder mentioned above.
Client Base Set Up
For the client service basic configuration, we have to add both the remote configuration and Eureka discovery client dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
And in the application.yml file, we will set the config.import parameter with the optional:configserver
value, in order to connect to the config server (no need to specify the http://localhost:8888 URL, since this is the default for the config server):
spring:
application:
name: client-service
config:
import: "optional:configserver:"
cloud:
config:
username: myusername
password: mypassword
Discovery Server Base Set Up
The discovery server also needs the spring-cloud-starter-config
dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
And the application.yml content will be similar to that needed for the client:
spring:
application:
name: discovery-service
security:
user:
name: myusername
password: mypassword
config:
import: "optional:configserver:"
cloud:
config:
username: myusername
password: mypassword
Specific Configuration for Client and Discovery Services
Having set the basic configuration of client and discovery components as above, we should set the more specific one inside the configuration server. We will use two configuration files stored in the /resources/config folder of the Spring Boot application mentioned before. The files are named after the application.name property of each application:
- /config/client-service.yml
- /config/discovery-service.yml
The content of client-service.yml will be:
server:
port: ${PORT:8080}
myproperty: value
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://myusername:mypassword@localhost:8760/eureka/
As we can see above, the defaultZone
property contains the URL of the Eureka server instance, with username and password inside it, since we have secured the discovery server by basic authentication, as the configuration server.
As for the discovery server remote configuration, i.e. the discovery-service.yml file, we will have the following content:
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
server:
port: ${PORT:8760}
Config First: Sample Projects from GitHub
You can find three sample projects with the above configuration on GitHub:
We can test the example by compiling and building the three projects with 'mvn clean install' and then executing the single jar
files from the command line:
java -jar spring-cloud-discovery-configserver-configfirst-1.0-SNAPSHOT.jar
...
java -jar spring-cloud-discovery-client-configfirst-1.0-SNAPSHOT.jar
...
java -jar spring-cloud-discovery-server-configfirst-1.0-SNAPSHOT.jar
Then we can check the discovery service dashboard, where we will see the single client instance registered as 'CLIENT-SERVICE', and check the client, for instance, by executing some REST service implemented by it.
Discovery First
In the Discovery First approach, the discovery service is not supposed to store its configuration remotely. On the contrary, it is the config server that registers itself on the discovery registry. As for the client service, it will also register itself on the discovery service, in order to fetch the properties needed to connect to the config server. In this architecture, the client needs to know just the application ID of the config server, and the security credentials if there are any.
The discovery service does not need the config server client dependency anymore, and its configuration can be stored entirely in its local application.yml file:
spring:
application:
name: discovery-service
security:
user:
name: myusername
password: mypassword
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
server:
port: ${PORT:8760}
The client and config server local setups are described in the two sections below.
Client Base Set Up
The Maven POM dependencies are the same as the Config First scenario, while the basic local configuration in the local application.yml file must contain the URL to the Eureka service.
spring:
application:
name: client-service
eureka:
client:
serviceUrl:
defaultZone: http://myusername:mypassword@localhost:8760/eureka/
Config Server Base Set Up
In addition to the spring-cloud-config-server
in the discovery first scenario, the config server application needs also the Eureka client dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
And the application.yml content will contain the discovery service URL, just like the client component:
server:
port: ${PORT:8888}
spring:
profiles:
active: native
application:
name: config-server
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://myusername:mypassword@localhost:8760/eureka/
Specific Configuration for Client Service
In the discovery first scenario, the config server local repository will contain only the client configuration file:
- /config/client-service.yml
And the content of client-service.yml will be:
server:
port: ${PORT:8080}
myproperty: value
spring:
cloud:
config:
discovery:
enabled: true
serviceId: config-server
eureka:
client:
serviceUrl:
defaultZone: http://myusername:mypassword@localhost:8760/eureka/
Here, besides the discovery service URL, which is needed because the client has to connect first to it, we can also see the spring.cloud.config.discovery.enabled property set to true
. This property makes the client application aware that it must connect to the config server by its metadata stored in the discovery service registry. The serviceId property stores the config server application ID which matches that stored in the discovery service registry.
We can conclude that in the discovery service scenario, the config server instance (or instances) can be changed to run in a different host/port without the need to modify the client configuration.
Discovery First: Sample Projects from GitHub
You can find below the sample projects' GitHub links for the Discovery First scenario:
We can test the example as we already did for the config first scenario.
Conclusion
In this post, we have completed the discussion started in the first part of the article and introduced some further topics, such as:
- Programmatic Java API
- REST API
- How to put a basic layer of security on discovery server and client applications
- Combine discovery features and remote configuration in two different ways: config first and discovery first.
You can find the source code for the examples in this article in the following GitHub module:
Published at DZone with permission of Mario Casari. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments