Creating Cloud-Native Microservices
Creating microservices that are able to work natively in a cloud infrastructure is something that we should look forward to when we design a microservices architecture that follows our microservices principles. we will learn about Cloud-Native microservices, and how Spring Cloud allows us to easily create them using a vendor-independent approach. To understand more about Spring Cloud, we will discuss how it works and how we can use it to create our microservices. Finally, we will cover the most important components that Spring Cloud provides.
Understanding Spring Cloud
Configuration server
Service discovery
Gateway
Understanding Spring Cloud
Modern microservices architecture focuses on deploying microservices into a cloud. This will enable a more optimal model for our products since we can grow or shrink the infrastructure as per our needs; however, the landscape of cloud providers has grown drastically, and many platforms need to tide our microservices to a particular vendor. Spring Cloud allows a vendor-agnostic approach that permits us to deploy our services in any cloud and take the full benefits of cloud computing, converting our microservices into Cloud-Native microservices.
What is a Cloud-Native application?
The cloud has very interesting capabilities. Applications that will be deployed in a cloud need to use cloud capabilities in order to work effectively. For example, in a cloud we should be able to scale our application; we can start three instances of a service if we need to increase the capacity of the application. We can increase the number of instances from three to five, but when we do this, we need to do it in a timely manner so that new instances are ready to work right away, therefore the application needs to be built with the ability to start quickly. In the same way, an application that is on a cloud may consume other services from the platform and should go through the platform capabilities. If we are invoking one service from another, since the instances are created or destroyed dynamically, we can't just have a static set of URLs to access them. Our application should query the cloud to find the services and their instances. Since we don't know when an instance will be created or destroyed, it is not so easy to synchronize data between the ones that are running, neither guarantee that different requests for a client or customer will always hit the same instance. Hence, we can not guarantee affinity in the same way that a traditional architecture can. We highlight this because we believe that a cloud application needs to be aware that it is running on a cloud, since the actions that will be performed need to be understood in the context that is used; the same things that work outside a cloud may not work in a cloud. For this, we need to create cloud-aware microservices. However, we can try to make an application that is not cloud-aware work in a cloud. For our previous example, when instances are created or destroyed, we can map them using a routing system external to the cloud, so our application can still use it, without using the cloud capabilities to discover services. We never recommend making things that are not cloud-aware to run in a cloud. If we are in a cloud, it is to fully utilize its benefits. If not, it's probably better that we choose a more traditional architecture. When we create a cloud-aware microservice that is used natively in the cloud where it's running, we are creating a Cloud-Native microservice. Spring Cloud allows us to easily create Cloud-Native microservices.
Spring Cloud architecture
Spring Cloud provides a framework to easily create Cloud-Native microservices with the benefit of having a vendor-agnostic approach, while the same components can be used seamlessly in a range of cloud providers. But before using it, we need to understand how the architecture of their components works. There are some key components in this architecture, but before understanding them, we will review the architectural patterns that will be used to build our Cloud-Native microservices:
Configuration server
Service discovery
Gateway
Circuit breaker
Configuration server Configuring our microservices is usually a complex topic, as we learned in Chapter 2, Getting Started with Spring Boot 2.0. We can use Spring configuration to simplify this, but it is still a complicated process. We can't just statically set the microservices configuration in our application code, since it can change when our cloud changes. We need a mechanism that allows our services to request their configuration. A Configuration Server provides the capability to be queried about configurations, so when a microservice starts, it can retrieve all the values that need to be configured, such as URL, database connections, password, and anything that can be configured, from the Configuration Server:
For example, let's imagine that we have a Configuration Server ready and two microservices that use it; the Products Microservice Instances and the Customers Microservice Instances. When new instances are created, they can retrieve their configuration from the config server. This allows us to easily create those service instances since all configuration required by our microservice will be provided by the Configuration Server. This speeds the creation of the new instance since they don't require any more configuration than the one provided by the Configuration Server.
Service discovery
In a cloud, we may need to connect to other services, but we don't actually know where this service is and how many instances are available. This requirement can be satisfied using a Service Discovery Server. Each instance of any service, when created, will be registered in the Service Discovery server. Thus, an application can ask what instance is available, but the Service Discovery needs to keep an up-to-date view of what is available. When the microservice is shutting down, it needs to be deregistered from the Discovery Service, so it gets removed from the known instances; however, a microservice could go down drastically (for example, in an application error) without informing the discovery service. This could result in a situation where the discovery service may have registered instances that are actually not working or no longer available. To avoid that situation, discovery services could use the Heart-beat mechanism. A Heart-beat is a way for a microservice to tell a Service Discovery that it is available and ready to work. It is like sending a request to the Discovery Server every five minutes to confirm that it is still alive. If the Discovery Server does not receive those Heart-beats from a microservice instance, it will assume the microservice is dead and removes it from the list of known instances of that microservice. Finally, with an updated list of microservice instances, it is possible for a microservice to find, when needed, the instance of any service. Let's visualize this with an example:
In this example, we have a Products Microservice that is registered in a Service Discovery Server, and will periodically notify that it is still up and running via a Heart-beat. When another microservice, such as customers in this example, asks for the Products Microservice, the Service Discovery Server will be able to answer back with the available instances of the Products Microservice.
Load Balancer
When we have a microservice that needs to send a request to a different microservice, we can use a Service Discovery to get the list of instances available of the services that we want to use. Then we may need to choose one instance among them to perform our request. Eventually, we may need to perform another request, and in that case, we may want to choose a different instance so our request is distributed evenly to the different instances. To achieve this, we can use the Load Balancer pattern. To perform, for example, a round-robin iteration on the instance of a microservice to distribute calls to them. So when we perform the first request, it will choose the first instance, for the next request, the second instance, and so on, until there are no more instances and we return back to the first. On top of that, we may need to add the information from our Service Discovery service, since the instances can be created and destroyed, and then register or unregister from the Service Discovery Server. We may need to check that the list of the instance that we are iterating is up to date and managed correctly. This will make an optimal usage of the instances of our microservices, and split the load among them evenly.
Gateway
When we need to expose our microservices to be used by any application, we may need to use the Gateway pattern. In a Gateway pattern, we have an entry point to our microservice whose mission is to simplify the access to them; instead, to make the consumer applications use a Discovery Server and a Load Balancer to find our microservice instances, we expose them using a simple route and managing the details for them. Let's draw an example to explore this topic further:
We have two microservices registered on our Service Discovery Server, Customers Microservice, and Products Microservice. When an application (for example, a web application) invokes our Gateway using the /products path, our Gateway can: Find all the instances of the Products Microservice using the Service Discovery Server Rotate among them using a Load Balancer Invoke the selected instance Return the result back to the calling application And if the web application uses the /customers path, we can send that request to the microservice customers instead. The web application does not need to use the Service Discovery directly, neither the Load Balancer, since this is done by the Gateway internally, giving a simple interface to the calling applications. Since our Gateway is an entry point to our microservice, we can use it to add a couple of benefits to our architecture. Our Gateway can be used as the entry point for our security, checking the credentials or privileges to access those microservices, and if those security mechanisms need to be changed, we will change them just in a single place—our access point, our Gateway. Decouple who implements a call, if tomorrow the /products path will be implemented by another microservice, we can change it in our Gateway without affecting the application that uses it. The Gateway pattern is a common pattern for enterprise applications that have existed for a very long time, but in today's cloud era, it makes more sense than ever before.
Circuit breaker
In our microservice, we may need to perform operations that can eventually fail. If those operations fail, we need to ensure that we are still able to answer our users, and that other parts of the software work as they should. Let's imagine that we have three microservices: The Opportunities microservice will return a list of customers and the offers tied to each of them The Customers microservices will return just customer information The Offers microservice will return offers information The Opportunities microservice will call the Customers microservices to get the customer information, and for each of the customers, it will call the Offers microservice to retrieve the offers tied to them. Let's assume that the Offers microservice is not working, and since Opportunities uses Offers every single time our Opportunities microservice is being called, we return an error back to the users of our microservice. In doing so, we may be overstressing the Offers microservice that we know is not working, but we continue calling it and getting an error. In fact, we may even decrement the performance of the Opportunities microservice; since waiting for a response from the Offers microservice may take a long time, we are leaking a problem from a dependency, Offers, into our Opportunities microservice. To prevent this from happening, we can encapsulate the operation by calling the Offers microservice from the Opportunities microservice in a circuit breaker. A circuit breaker can have an open or closed status, telling us if the operation that encapsulates can be used—think of circuit breakers as fuses in an electrical installation. When the operation fails, we can just close the circuit for a period of time, then if new requests are coming to our Opportunities microservice since the circuit is closed, we just return an error without sending a request to the Offers microservice. After a certain amount of time, we can check whether the Offers microservice is working again, opening the circuit and allowing to return valid responses again, but in the period of time that the circuit was closed, we were given time for the Offers microservice to recover. But we can find other benefits in this technique.A circuit breaker allows us to define a fallback mechanism. If we can't answer with the offers since the circuit is closed, we could just return a default set of offers to whoever invokes our Opportunities microservice. This will allow our application to still show some information. In most cases, this is much better than just an error without any information at all. We always need to ensure that a failure in one part of our application does not make other parts, or even the whole application, fail. Circuit breakers are a perfect mechanism for this, even in the worst scenarios.
Spring Cloud Netflix
Netflix is one of the big contributors to open source initiatives. They are a company with a tremendous focus on microservices, so within the Netflix OSS (Open Source Software), they originally created many components that implement the cloud architecture pattern, and we can easily use them with Spring Cloud since doing so allows us to choose the implementation of the pattern. Let's review the ones that we are going to use: Eureka: A Service Discovery service that we can easily use to register or find instances of our microservices. Ribbon: A configurable software Load Balancer that can be integrated with Eureka to distribute calls within the microservices instances. Hystrix: A configurable circuit breaker, with a fall-back mechanism, that we can use when creating microservices. Zuul: A Gateway Server that uses Eureka, Ribbon, and Hystrix to implement the Gateway pattern.
Now Lets check our code : It will make more sense.
1. Config_Server :
package com.microservices.configserver
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.config.server.EnableConfigServer
@SpringBootApplication
@EnableConfigServer
class ConfigServerApplication
fun main(args: Array<String>) {
runApplication<Chapter06ConfigServerApplication>(*args)
}
under resources: config folder:
microservice:
example:
greetings: "hello from config server"
zuul:
ignoredServices: '*'
routes:
greetings:
path: /message/**
serviceId: greetings
microservice:
example:
greetings: "{cipher}7b310fe9e22a913d6a21dd768a616f9700ba4bde6f1879b4d82a5a09ea8344a4"
microservice:
example:
greetings: "hello from another profile"
application.yml
server:
port: 8888
spring:
profiles:
active: native
cloud:
config:
server:
native:
search-locations: classpath:config/
bootstrap.yml
encrypt.key: "this_is_a_secret"
spring:
cloud:
config:
server:
encrypt:
enabled: false
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microservices</groupId>
<artifactId>chapter06-config-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>chapter06-config-server</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.M5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jre8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
2.Discovery_Server :
package com.microservices.discoveryserver
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
class Chapter06DiscoveryServerApplication
fun main(args: Array<String>) {
runApplication<Chapter06DiscoveryServerApplication>(*args)
}
application.yml
server:
port: 8761
spring:
application:
name: "discovery-server"
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microservices</groupId>
<artifactId>chapter06-discovery-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>chapter06-discovery-server</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.M5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jre8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
3.Gateway :
package com.microservices.gateway
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.zuul.EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
class Chapter06GatewayApplication
fun main(args: Array<String>) {
runApplication<Chapter06GatewayApplication>(*args)
}
application.yml
spring:
application:
name: "gateway"
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microservices</groupId>
<artifactId>chapter06-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>chapter06-gateway</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.M5</spring-cloud.version>
</properties>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jre8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>