In this section we will describe how we work at BEEVA with microservices.
-
2.1. Overview for a microservice architecture
-
Microservices with Spring Cloud
3.1. Introduction
3.2. Config Server
3.3. Eureka
3.4. Zuul
3.5. Ribbon
3.6. Bus
3.7. Sidecar
A microservice is an independent process with, at least, the following features:
- Loosely coupling
- Small logic
- Single purpose
- Independent deployment
- Automated deployment
Microservice based applications leverage of several advantages:
- Isolated logic in functional blocks
- Easier maintenance
- Selective deploys without outages for the whole architecture
- Easier integration with cloud platforms (for example, fine grained scaling)
This document addresses some of the challenges that arise when using a microservice-based architecture:
- Microservice discovery : how many microservices do exist and where do they are?
- Central configuration
- Error resiliency and fail-over
All these challenges are addressed using a good infrastructure, able to orchestrate all existing microservices in our application.
"As small as possible but as big as necessary"
Definition: A software architecture design pattern in which complex applications are composed of small, independent processes communicating with each other using language-agnostic APIs.
Monolithic | Microservice | |
---|---|---|
Architecture | Built as a single logical executable (typically the server-side part of a three tier client-server-database architecture) | Built as a suite of small services, each one running separately and communicating with lightweight mechanisms |
Modularity | Based on language features | Based on business capabilities |
Agility | Changes to the system involve building and deploying a new version of the entire application | Changes can be applied to each service independently |
Scaling | The whole application has to be scaled horizontally behind a load-balancer | Each service scaled independently when needed |
Implementation | Typically written in one language | Each service implemented in the language that best fits the need |
Maintainability | Large code base overwhelming new developers | Smaller code base easier to manage |
Transaction | ACID | BASE |
Service-Oriented Architecture (SOA) sprung up during the first few years of this century, and microservice architectures (MSA) bear a number of similarities. Both types are a way of structuring many related applications to work together, rather than trying to solve all problems in one application. Traditional SOA, however, is a broader framework and can mean a wide variety of things. Some microservices advocates reject the SOA tag altogether, while others consider microservices to be simply an ideal, refined form of SOA.
The typical SOA model usually depends on ESBs while microservices use faster messaging mechanisms. SOA also focuses on imperative programming, whereas microservice architectures focus on a responsive-actor programming style. Moreover, SOA models tend to have an outsized relational database, while microservices frequently use NoSQL or micro-SQL databases. But the real difference has to do with architecture methods used to arrive at an integrated set of services in the first place.
Since everything changes in the digital world, agile development techniques that can keep up with software evolution demands are invaluable. Most of the practices used in microservice architectures come from developers who have software applications for large enterprise organizations, and who know that today’s end users expect dynamic yet consistent experiences across a wide range of devices. Scalable, adaptable, modular, and quickly accessible cloud-based applications are in high demand. And this has led many developers to change their approach.
- Scalability and Reliability:
Constrain the size limit of individual cases and allow to have all behaviours in mind.
Technical debt is kept under control, and code is thus able to evolve. Going through service calls to communicate with other areas formalizes exchanges.
Interface contracts are then more strict, and it is easier to consider all cases, including error cases.
- Horizontal Scalability:
With limited sized applications it is easier to increase scalability by refactoring the code or by rewriting it completely based on new requirements.
- Technological innovation:
Code bases and teams are independent and can therefore make technical choices according to their own needs.
- Business innovation:
If all information systems are structured using services, it is easy to experiment by starting a new project based on others’ data and easier to remove features because it will not affect the whole project.
While microservice architectures have lots of advantages, they have several limitations and requirements as well.
- The system becomes distributed
Conventional architectures make possible to ensure having independent states between different applications: everyone is the owner of his business field. When switching to microservices, the system becomes widely distributed. This introduces new particularly difficult issues.
The most complicated case is about transactions: each time a transaction is shared between two applications, we must manage transactions in two phases or manage cancellations. In a service based system, there is no such tool that allows to do this automatically. We must do it manually wherever is needed in our code.
And even when you can bypass transaction: there are always references to cross-application data, and therefore a management system for asynchronous events or cache that allows to ensure data consistency.
Talking about external services unavailability, the architecture design must be implemented assuming that such services can fail arbitrarily. This is the only way of reducing risks when depending on third party or external services.
It is also important to pay attention to all service quality (SLA) for different applications in order not to be surprised.
Eventually the system becomes more difficult to test: integration tests increase, and require more complex data preparation, and also a wider battery of test cases with both technical and business errors to be tested.
- Value-added services
Although the REST approach suggests to handle simple features, there is always a proportion of calls with “value-added” that involve multiple business areas.
Regarding microservices, it means performing calls between several applications.
This has the effect of multiplying error cases to manage (problem of distributed systems) and adding network latencies.
In most critical cases, it becomes necessary to add specific services in different applications or add data caches, causing consistency issues.
- DevOps and provisioning
Multiplying the number of independently deployed applications implies multiplying the number of deployments and server instances aswell.
To avoid errors and excessive additional costs, we need a very efficient workflow in terms of tools and processes with as much automated deployments as possible. This applies even more for tests and PoCs where we want temporary environments as sandbox.
The fundamental SOA approach is to keep control of organisational and business complexity by distributing it.
By separating the projects, the complexity is reduced in exchange of an extra cost in other places, including having a distributed system.
You can have well-organized, scalable, evolutive monoliths… but it requires strong discipline at all times. Microservice architectures choose not to take those risks to make sure everything is under control.
However, if this is implemented in an inappropriate environment or in a bad way, we will combine disadvantages without enjoying the benefits, and therefore we will take much higher risks than in conventional service architectures.
So, they real question to be posed is not if you need or not microservices but...
- Do you have issues that this approach solves?
- Do you fulfill needed requirements for a microservice architecture?
- Are you ready to deal with an architecture like this?
Finally, never forget that an architecture is a tool that we adapt to our needs and not a dogma to follow: if what suits you is an hybrid solution taking some microservices ideas and leaving other ones aside, go for it!
Once decided that a microservice-based architecture is the right solution, we need to find a way to setting it up.
Although there is no magic recipe, some approaches seem to emerge.
THE DIFFICULT CASE: FROM SCRATCH
The most attractive situation is to create a new system from scratch, nothing to challenge or to manage, this seems the ideal situation. Unfortunately, building microservices from scratch is the most difficult case:
- It is complicated to determine the limits where we need to cut out the different projects because it is not clear how the system will evolve.
- As we have already seen, the evolutions are expensive because you have to make cross-project refactoring.
Unless you are a veteran in this matter, it is better starting with a monolithic approach.
THE FAVORABLE CASE: PEEL A MONOLITH
The most favorable case is the monolith that we “peel”. After examining its organisation and structure, we will isolate pieces to the edge of the system following the cutting lines that emerged naturally.
Rather than ending up with 50 mini projects, the goal is:
- Having one or several “core” applications of average size, consistent with each other
- Microservices moving around, which are going to move away with time
This operation is made easier as the initial application is well structured in technical layers, business bricks and that this reorganisation is respected. The best practices of software developments allow to have “microservices-ready” projects. Otherwise, it takes a lot of investigation to extract some parts of the code.
Automated tests are essential to limit risks. In their absence, it is necessary to consider the application as a legacy and use the proper techniques to remove the technical debt.
Before getting into the “cutting” phase, we must examine the data distribution issues: this is the most structural element and by itself can lead to a complete failure if not well analysed.
Finally, we must avoid to be dogmatic considering that the operation is necessarily one-sided.
Sometimes, the evolution of projects could lead us to considering merging back two or more projects. Rather than a failure, this should be considered as a good sign, because it is a proof that your system is able to adapt to business changes.
A microservice-based application is distributed, which means that components have to interact with each other in order to achieve a common goal or purpose. Additionally, in an architecture like this is almost mandatory to have a high availability and excellent autoscaling mechanisms.
- Service Discoverer
A very important component of this kind of architectures is the service discovery. It provides a way of registering and finding each single available microservice in the system (basically, IP address and port).
Due to autoscaling, the number of microservice's instances will be dynamically modified depending on the traffic requirements, failures, updates, ... so this locations can not be static at all (like they would in a classical architecture).
There are two main service discovery patterns: client-side discovery and server-side discovery.
- Centralized Router
A centralized router proxies all traffic, providing a load balancer aswell. Usually, this router is the entry point to the system and communicates with service discoverer to find the actual microservice locations.
To successfully orchestrate, monitor and debug a microservice-based architecture, where microservices communicate with other microservices in the architecture, at least we will need:
- A way of quickly identify the exact location of potential errors or problems (for example, a flow monitoring tool)
- A consistent health status system that will inform consumers when the system is stable and, if problems are reported, what consequences do they have for them.
- Optional, but strongly recommended, is the use of a mature framework that helps us to handle orchestration. There are already some of them for different programming languages (Spring Cloud, GoKit, Seneca, ...), and as time goes by more and more will appear.
Additionally, there are a couple of development considerations that will ease the use of a microservice-based architecture:
- Do not forget applying development good practices. Specially in distributed systems, these good practices are sometimes harder to implement, but the effort will be worthy.
- Keeping a good trade-off between innovation and sustainability. More innovation in a project usually means more potential problems or challenges.
- Although every microservice is independent, it is very important to try that every developer keep a general understanding of the system as a whole. Otherwise, knowledge will remain centralized on several developers and maintainability will be harder.
- Despite every microservice will be developed by different developers and maybe different teams, it is very important to keep the code as homogeneous as possible. There should not be too many differences for a new developer to review one microservice's code or another.
Below is a list of some technologies related with microservice-based architectures:
Component | Technologies |
---|---|
Routing | Apache, HAProxy, Zuul, DNS, ELB |
Configuration | Consul, Zookeeper, Spring Cloud Config Server, Etcd |
Discovery | Consul, Zookeeper, Spring Cloud Eureka, DNS, Registrator |
Packaging | Docker |
Container orchestration and clustering | Marathon, Kubernetes, AWS ECS, Docker swarm |
In a distributed system where several components (microservices) need to communicate with each other, latency can be a serious problem. The more microservices involved in the inter-communication process, the bigger the impact of latency. Without proper timeout mechanisms, the entire application could be collapsed.
Maybe the bigger difference between microservice-based and SOA architectures is the impact of side effects.
Microservice-based architectures build small pieces of functionality relying on repeatable results, input/output standard mechanisms and an exit code that informs about if the operations resulted in success or failure. This behaviour is similar to classical Unix pipes, where every member of the pipeline is responsible of deciding if its input is acceptable or not, but there are not any control statements.
Any change in a microservice could lead to a cascade of dependent changes that need to be done at once and deployed synchronously, potentially requiring approval of different teams.
Microservice intercommunication is performed using synchronous (HTTP/REST) or asynchronous (AMQP) protocols.
Synchronous dependencies between services imply that caller is blocked waiting for a response before continuing it's operation. This tight coupling reduces scalability and could lead to an undesired error chain.
With asynchronous communication, caller does not blocks after a request, being able to continue operating after spawning a new thread that will wait for callback calls for the asynchronous operation.
Any problem raised for the asynchronous operation will not affect the caller or its normal operation, reducing coupling and improving scalability at the same time.
Instead of start building microservice-based architectures from the scratch, it is strongly recommended to use a framework that eases orchestration and handling processes.
It is important to use a mature framework, like the one released by Netflix as part of Netflix OSS stack. This stack was wrapped by Spring in a project, called Spring Cloud.
Spring Cloud and Spring Boot together offer a really easy way of deploying core and orchestration components, including:
- A centralized configuration server
- A service discovery component (Eureka)
- A router (Zuul)
- A load balancer module (Ribbon)
Complemented with some additional components that enhance available features:
- A fail-over mechanism based on the circuit breaker pattern (Hystrix)
- A communication bus to send messages to all microservices (Spring Cloud bus)
- A bridge module to allow polyglot microservices, implemented in programming languages different than Java (Sidecar)
The picture below illustrates a general overview of this architecture:
One of the most important orchestration modules is the Config Server. Its main purpose is to centralize the configuration for all microservices using the architecture.
We recommend to use GIT as source for the properties and to have an optimal file organization. Additionally, we should follow some recommendations:
- Use an isolated repository for spring cloud components (zuul, eureka)
- Use folders to categorize property files instead of a flat folder for every file
- Use profiles to swap easily between environment configurations.
- Do not duplicate properties, group the common ones and store them in files at the root level
- If you need to use several GIT repositories, associate them to microservice name patterns
Additionally, we recommend to avoid Single Points Of Failure (SPOF) providing several configuration servers behind a load balancer (see section 5 for details).
A sample configuration file (bootstrap.yml) for a Spring Cloud Config Server is shown below:
spring:
application:
name: configsrv
cloud:
config:
server:
git:
uri: git@mydomain:mygroup/mydefaultrepository.git
repos:
myrepository1:
pattern: project1-*
uri: git@mydomain:mygroup/myrepository1.git
searchPaths: functionality-*
myrepository2:
pattern: project2-*
uri: git@mydomain:mygroup/myrepository2.git
searchPaths: functionality-*
defaultLabel: master
and the associated file structure for one of the repositories, according to the previous configuration is listed below:
application.yml
application-local.yml
application-pre.yml
application-pro.yml
project1-arbitraryname/functionality-1/microservice1.yml
project1-arbitraryname/functionality-1/microservice1-local.yml
project1-arbitraryname/functionality-1/microservice1-pre.yml
project1-arbitraryname/functionality-1/microservice1-pro.yml
project2-anotherarbitraryname/functionality-2/microservice2.yml
project2-anotherarbitraryname/functionality-2/microservice2-local.yml
project2-anotherarbitraryname/functionality-2/microservice2-pre.yml
project2-anotherarbitraryname/functionality-2/microservice2-pro.yml
When a microservice with name microservice1 starts with profile 'local', it will request to the config server all needed configuration files. According to the configuration above, config server will server the following files:
- application.yml
- application-local.yml
- microservice1.yml
- microservice1-local.yml
You can secure the config server to add an extra layer of security. You can use the default basic security HTTP configuration including spring security in the classpath (spring-boot-starter-security).By default it uses a username "user" and a randomly generated password. It is a good practice to set a password for the property (security.user.password) and to encrypt it. If you want to change the username you have to make use of the property (security.user.name)
It is possible to encrypt the properties (values beginning with {cipher}), which will be decrypted before sending to clients over HTTP. There are 2 enpoints /encrypt and /decrypt to encrypt or decrypt values
$ curl localhost:8888/encrypt -d mysecret
682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
$ curl localhost:8888/decrypt -d 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
mysecret
Eureka is a module responsible for both service registering and service discovering. It is comprised of two modules:
- Eureka Server
- Eureka Client
It is a REST service that is used for service discoverying, load balancing and fail-over of middle-tier servers. For a complete reference of all Eureka REST operations, see https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
Eureka Server can be deployed in a Single Instance configuration or (recommended) in a Cluster Configuration.
When Eureka is deployed as a Cluster, all nodes in the cluster need to communicate with each other to synchronize their metadata, so when a new microservice registers with one of the Eureka Server nodes, all metadata information will be replicated along the cluster.
Each Eureka Server node shares the same information for each microservice. To achieve this, each Eureka Node keeps a registry with the information of each registered microservice. Each microservice is responsible for providing a heartbeat to let Eureka know that it is alive and running. When some microservice fails providing this heartbeat signal, it is removed from the registry.
A high level example of a cluster configuration, can be seen below:
Eureka server exposes information of every single microservice registered via REST API (/eureka/apps
) and the Web UI.
Eureka server is included in Spring Cloud as an annotation: @EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
public static void main(String[] args){
new SpringApplicationBuilder(EurekaServer.class).web(true).run(args);
}
}
Eureka Server Standalone configuration
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
waitTimeInMsWhenSyncEmpty: 0
instance:
leaseRenewalIntervalInSeconds: 30
Eureka server AWS configuration
server:
port: 8761
eureka:
client:
region: eu-west-1
preferSameZone: false
registerWithEureka: true
fetchRegistry: true
useDnsForFetchingServiceUrls: true
eurekaServerDNSName: eureka.internal.com
eurekaServerPort: 8761
eurekaServerURLContext: eureka
server:
waitTimeInMsWhenSyncEmpty: 0
instance:
leaseRenewalIntervalInSeconds: 30
It is a JAVA-based client component that wraps all necessary requests to interact with Eureka's REST API.
When a microservice needs to be registered in Eureka Server, it has to provide some metadata information (host, port, health URI, etc.). As mentioned above it also has to provide a heartbeat every 30 seconds to let the cluster knows that is up and running.
Eureka client is included in Spring Cloud as an annotation: @EnableEurekaClient.
It is very important to understand how Eureka Clients and Eureka Server communicate between then. Involved actors (Eureka Server, Eureka Clients, Ribbon Clients) use cache mechanisms, this means that it will take a time for microservice changes (registrations, updates, ...) to be effective.
Both @EnableEurekaServer and @EnableEurekaClient provide an implementation of Eureka Client, along with an implementation of Ribbon Load Balancer. So, even when we are not directly using some of these clients, they are there for us.
@SpringBootApplication
@EnableEurekaClient
public class HelloApplication {
public static void main(String[] args){
new SpringApplicationBuilder(HelloApplication.class).run(args);
}
}
Eureka Client AWS Configuration
eureka:
client:
region: eu-west-1
preferSameZone: false
registerWithEureka: true
fetchRegistry: true
useDnsForFetchingServiceUrls: true
eurekaServerDNSName: eureka.internal.com
eurekaServerPort: 8761
eurekaServerURLContext: eureka
initialInstanceInfoReplicationIntervalSeconds: 10
instanceInfoReplicationIntervalSeconds: 5
serviceUrl:
defaultZone: http://eureka.internal.com:8761/eureka
registryFetchIntervalSeconds: 5
availabilityZones:
default: defaultZone
instance:
metadataMap:
instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
When a microservice boots up it sends the first heartbeat to Eureka Server. The microservice does not appears in Eureka Server yet, so any request will return a 404 status code.
A second heartbeat will provide Eureka Server microservice information (host, port, ...) but it is not yet available for processing incoming requests, until a third heartbeat is sent, moment in which microservice is ready.
Time interval between heartbeats is 30 seconds by default. There is a property that allows to modify this value (eureka.instance.leaseRenewalIntervalInSeconds
).
Eureka Server maintains a cache containing all sent responses, refreshed by default after 30 seconds. After the microservice is registered in Eureka Server, could take a while until it appears in /eureka/apps
endpoint's response.
This behaviour can be modified with property: eureka.server.response-cache-update-interval-ms
Each Eureka Client maintains a local cache to avoid sending too many requests to Eureka Server's registry. By default, this cache is refreshed every 30 seconds. Again, this can be configured through the property: eureka.client.registryFetchIntervalSeconds
As previously mentioned, each Spring Cloud microservice has an implementacion of Ribbon's LoadBalancer, that retrieves its information from the local Eureka Client, but it also mantains a local cache to avoid to overload the discovery client in Eureka. As you can imagine, this property is set by default to 30 seconds. We can customize this value through the property: ribbon.serverListRefreshInterval
Important note Unless you know exactly what you are doing, we strongly discourage modifying default time intervals, as unexpected and unpredictable behaviours could occur in several components of the orchestration layer.
For further details, there is an open discussion in both Spring Cloud's Github and Stackoverflow
Zuul is the main entrypoint to the microservice architecture. It is responsible for routing each request to the corresponding microservice. Zuul is built to enable dynamic routing, monitoring, resiliency and security.
As part of the Spring Cloud framework, Zuul has both an Eureka Client and a Ribbon client built-in implementation. By default, this Ribbon implementation uses a Round Robbin algorithm for routing requests, but this algorithm can be changed and customized based on the needs. For example we can implement a routing algorithm based on the latency of the responses or based on the geographical location of each microservice.
Zuul consists of filters that are responsible for different tasks based on the type of each one of them. These filters are written in Groovy, but Zuul supports any JVM-language.
There are four standard types of filters that are related to the lifecycle of a request (see the diagram below):
- PRE Filter: These type of filters are executed before the request arrives at the destination
- ROUTING Filter: These type of filters handle routing request to an origin. Here is where Ribbon Client acts
- POST Filter: These type of filters are executed after the request has been routed to the destination. For example, we can modify some aspects of the response as headers, before sending it back to clients
- ERROR Filter: When an error arises from one of the previous described filters, this type of filter comes into action
There is another type of filter that allows us to create new and custom filters that executes explicitly, that is, these filters send a response to the request themselves and prevent the request from spreading to underlying microservices. This behaviour can be useful to define some endpoints for health checking purposes. Zuul Special Filters
If You want to know more about how Zuul and Filters work, check the official Netflix Github documentation at: Zuul - How it Works
Zuul is included in Spring Cloud as an annotation: @EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
public class ZuulServer {
public static void main(String[] args){
new SpringApplicationBuilder((ZuulServer.class)).web(true).run(args);
}
}
spring:
application:
name: zuul
server:
port: 8989
eureka:
client:
registryFetchIntervalSeconds: 5
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
leaseRenewalIntervalInSeconds: 5
healthCheckUrlPath: /health
Ribbon is a client side library to communicate microservices, Eureka, Zuul and the rest of the components with each other.
It provides the following features:
- Load Balancing: The main purpose that we are explaning in this guide
- Fault Tolerance: Request retries when something goes wrong
- Multiple Protocol (HTTP, TCP, UDP) Async or Reactive
- Caching as mentioned above
As we have seen before, Ribbon is widely used in Zuul, Eureka and Microservices implementation.
An example of a Ribbon configuration for Zuul:
zuul:
ribbon:
ServerListRefreshInterval: 1000
ReadTimeout: 20000
ConnectTimeout: 5000
If you want to know more about Ribbon, check the official Netflix Github documentation at: Ribbon
Spring Cloud Bus enables communication between nodes in a distributed system throughout a message broker using asynchronous protocols such as AMQP.
In that way, when a microservice wants to send a message to another microservice, it can send it throughout the bus straight to the microservice (all nodes of one microservice as defined in Eureka), or it can broadcast that message to all of the microservices listening in the bus. Each microservice can process the message or ignore it.
Currently there is only one implementation for Spring Bus and is based in an AMQP broker as the transport. By default it uses RabbitMQ as a ConnectionFactory
, so we must ensure that RabbitMQ is installed and ready.
To enable Spring Bus we have to add the dependency spring-cloud-starter-bus-amqp
and Spring Boot will automatically load the configuration. We can, additionally, configure some parameters as shown:
spring:
rabbitmq:
addresses: amqp://localhost:5672
Or
spring:
rabbitmq:
host: localhost
port: 5672
username: user
password: secret
This kind of communication between microservices is really useful, for example when we want all the microservices to read a new property available in the Config Server. To allow this feature in a Microservice, we have to annotate that microservice with: @RefreshScope Spring Cloud annotation.
If you want to know more about Spring Cloud Bus, check the official Spring Cloud Github documentation at: Spring Cloud Bus
So far, we have only considered microservices implemented in JAVA programming language. What could I do if we want to build a microservice in a different programming language? the answer is : use the Sidecar module of Spring Cloud, based on Netflix Prana.
A bridge application, hosted in the same machine that the microservice is responsible of communicating the microservice with the Spring Cloud architecture.
We have to enable an endpoint inside our microservice to allow Eureka to monitor and register the microservice into the architecture. For communications in the other direction, the microservice can retrieve information from Spring Cloud via the bridge application.
http://[bridge_domain]:[bridge_port]/hosts/[serviceId]
But config server will not be accessible this way unless we register it in Zuul (which is not the default behaviour).
Suppose we configure a bridge application in port 10001.
Application.java
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.sidecar.EnableSidecar;
@SpringBootApplication
@EnableEurekaClient
@EnableSidecar
public class Application
{
public static void main( String[] args )
{
SpringApplication.run(Application.class, args);
}
}
application.yml
server:
port: 10001 # Port used by bridge application
spring:
application:
name: microservice-sidecar-bridge # Name that will be registered in eureka for the bridge application
sidecar:
port: 3000 # Microservice's port
health-uri: http://localhost:${sidecar.port}/health # Microservice's heartbeat url
app.js
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
var session = require('express-session');
// Populates req.session
app.use(session({
resave: false,
saveUninitialized: false,
secret: 'atomic mouse'
}));
var path = require('path');
var favicon = require('serve-favicon');
var cookieParser = require('cookie-parser');
var swig = require('swig');
var util = require('util');
var restify = require('restify');
// App engine
app.engine('html', swig.renderFile);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
var port = 3000;
// Start server
app.listen(port);
// Exports the configuration
module.exports = {
app: app,
};
//Add routes to detect the enabled endpoints
require('./routes/index');
/routes/index.js
var appJs = require('../app.js');
/* GET home page. */
appJs.app.route('/').get(function(req, res) {
res.send("This is the home page of node.js application");
});
appJs.app.route('/health').get(function(req,res){
res.set("Content-Type","application/json");
res.status(200).send({
"status":"UP"
});
});
Assuming that Zuul is started at port 8989 and with name zuul, an invocation to the node.js microservice would be possible :
and from the node.js microservice, retrieval of information about zuul would be possible through:
Docker is all about making it easier to create, deploy, and run applications by using containers. Containers allow a developer to package up an application with all of the parts it needs, such as libraries and other dependencies, and ship it all out as one package. By doing so, thanks to the container, the developer can rest assured that the application will run on any other Linux machine regardless of any customized settings that machine might have that could differ from the machine used for writing and testing the code.
In a way, Docker is a bit like a virtual machine. But unlike a virtual machine, rather than creating a whole virtual operating system, Docker allows applications to use the same Linux kernel as the system that they're running on and only requires applications be shipped with things not already running on the host computer. This gives a significant performance boost and reduces the size of the application.
An image is an inert, immutable, file that's essentially a snapshot of a container. Images are created with the build command, and they'll produce a container when started with run. Images are stored in a Docker registry such as registry.hub.docker.com. Because they can become quite large, images are designed to be composed of layers of other images, allowing a miminal amount of data to be sent when transferring images over the network.
In order for Java developers to test their applications in containers they typically have to build their code, create an image and run a container. You can use the Docker command line interface to manage images and containers. There are actually several Maven plugins for Docker. A rhuss/docker-maven-plugin simple configuration for this task
<plugin>
<groupId>org.jolokia</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.13.4</version>
<configuration>
<images>
<image>
<names>simple-service-app</names>
<alias></alias>
<build>
<assembly>
<mode>dir</mode>
<basedir>${basedir}/target</basedir>
<dockerFileDir>${basedir}/target</dockerfileFDir>
</assembly>
</build>
<run>
<ports>
<port>8080:8080</port>
<port>433:433</port>
</ports>
</run>
</image>
</images>
</configuration>
</plugin>
With this plugin you can configure run expecific docker commands an tune different docker settings as logs, tags, names... even execute commands.
We can add inside tag a where we can execute commands to run our app
<cmd>
<shell>java -jar /maven/${project.build.finalName}.jar server /maven/docker-config.yml</shell>
</cmd>
With the plugin you can build images and run containers and also easily remove these again which is especially important in the iterative development phase.
Build image:
mvn docker:build
Run container:
mvn docker:start
Stop and remove container:
mvn docker:stop
Remove image:
mvn -Ddocker.removeAll docker:remove
With a programming metaphor, if an image is a class, then a container is an instance of a class—a runtime object. They are lightweight and portable encapsulations of an environment in which to run applications.
Each Dockerfile is a script, composed of various commands (instructions) and arguments listed successively to automatically perform actions on a base image in order to create (or form) a new one. They are used for organizing things and greatly help with deployments by simplifying the process start-to-finish.
Dockerfiles begin with defining an image FROM which the build process starts. Followed by various other methods, commands and arguments (or conditions), in return, provide a new image which is to be used for creating docker containers.
A docker file looks like
#
# Oracle Java 8 Dockerfile
#
# https://github.com/dockerfile/java
# https://github.com/dockerfile/java/tree/master/oracle-java8
#
# Pull base image.
FROM dockerfile/ubuntu
# Install Java.
RUN \
echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
add-apt-repository -y ppa:webupd8team/java && \
apt-get update && \
apt-get install -y oracle-java8-installer && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /var/cache/oracle-jdk8-installer
# Define working directory.
WORKDIR /data
# Define commonly used JAVA_HOME variable
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
# Define default command.
CMD ["bash"]
One option to release a docker image inside our software process is :
Linking allows containers to exchange information (like IPs or ports) without exposing them on the host system. Fortunately, the docker-maven-plugin allows us to comfortably link containers together. The only thing we have to do is to add an element in the block of our microservice image configuration.
<image>
<alias>mongodb</alias>
<name>mongo:2.6.11</name>
<run>
<namingStrategy>alias</namingStrategy>
<cmd>--smallfiles</cmd>
<wait>
<log>waiting for connections on port</log>
<time>10000</time>
</wait>
<log>
<prefix>MongoDB</prefix>
<color>yellow</color>
</log>
</run>
</image>
<run>
...
<links>
<link>mongodb:db</link>
</links>
</run>
<execution>
<id>push-to-docker-registry</id>
<phase>deploy</phase>
<goals>
<goal>push</goal>
</goals>
</execution>
mvn -Ddocker.username= -Ddocker.password= deploy
To push your image to your own registry instead of Docker Hub just set the docker.registry.name property. Let’s assume your docker registry runs on your local machine on port 5000:
<docker.registry.name>localhost:5000/</docker.registry.name>
- Introduction to Microservices
- Wikipedia
- Spring Cloud Project Website
- Spring Boot
- How we ended up with microservices
- Microservices. The good, the bad and the ugly
- Monolith First
- Xebia Blog
BEEVA | Technology and innovative solutions for companies