This example demonstrate how we can build microservice project step by step.
Optima Growth is a software development company whose core product, Optima Stock (that we’ll refer to as O-stock), provides an enterprise-grade asset management application. It furnishes coverage for all the critical elements: inventory, software delivery, license management, compliance, cost, and resource management. Its pri- mary goal is to enable organizations to gain an accurate point-in-time picture of their software assets. The company is approximately 12 years old. The company wants to rebuild their core product, O-stock. While much of the business logic for the application will remain in place, the application itself will be broken down from a monolithic architecture to a much smaller microservice design, whose pieces can be deployed independently to the cloud. The replatforming involved with O-stock can be a “make or break” moment for the company.
- configserver. Port 9071
- eurekaserver. Port 9070
- gatewayserver. Port 9072
- licensingservice. Port 9180
- organizationservice. Port 9081
- All work with services takes place in containers. Therefore, first, we run the script to build containers - createDockerImages.sh in root of project
- When all containers are running, open KeyCloak http://localhost:8080/
- Create new user
- Set user password
- Set user role
- In realm tab set frontend URL - http://keycloak:8080/auth (keycloak it is inner alias in docker container)
- Before you start using the service, you need to get a JWT token. Push get request to http://localhost:8080/auth/realms/spmia-realm/protocol/openid-connect/token and copy access_token from response body. This token we will use like bearer authentication token for all request.
- For check current health
http://localhost:8072/actuator/health
- For check current routes
http://localhost:8072/actuator/gateway/routes
- Changed routes:
- For licensing-service added custom path - licservice
- For organization-service added custom path - orgservice
- If you want to change routes, you can do it in configuration file - gateway-server.yml
- Gateway redirect all request automatically (no need any configuration). Request example:
http://<gateway_host:port>/<service-name>/<service-resource>
http://localhost:8072/licservice/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3/license/f2a9c9d4-d2c0-44fa-97fe-724d77173c62/rest
- Get organization
http://localhost:8072/orgservice/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3
- Get license
http://localhost:8072/licservice/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3/license/f2a9c9d4-d2c0-44fa-97fe-724d77173c62/rest
- Get organization
http://localhost:8081/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3
- Get license
http://localhost:8180/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3/license/f2a9c9d4-d2c0-44fa-97fe-724d77173c62/rest
- Open kibana http://localhost:5601/ and create index. How to create index described in label "Sleuth, Zipkin and ELK"
-
Create new module "license server" with dependencies:
- Actuator
- Web
- lombok
- HATEOAS (this service return link how we can work with resource (GET, CREATE, UPDATE, DELETE))
- spring-cloud-starter-config (client for work with cloud config server)
- spring-boot-starter-data-jpa
- postgresql
- spring-cloud-starter-bootstrap (for use bootstrap.properties)
- flyway. Flyway migration. Not need additional configuration, only enable it in spring properties file. Spring automatically found migration file and execute it. You do not need flyway plugin!
- resilience4j-spring-boot2. We can test it if try to connect http://localhost:8180/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3/license/ a few times
- spring-boot-starter-aop. It is needed for resilience4j
-
Create class com/optimagrowth/license/controller/LicenseController.java with @RestController and @RequestMapping( value = "v1/organization/{organizationId}/license")
- API:
- v1/organization/{organizationId}/license/{licenseId} - GET license
- v1/organization/{organizationId}/license/ - PUT update license
- v1/organization/{organizationId}/license/ - POST create new license
- v1/organization/{organizationId}/license/{licenseId} - DELETE license
- API:
-
Create model class com/optimagrowth/license/model/License.java.
-
Create service class com/optimagrowth/license/service/LicenseService.java
-
Create config class that can read property from application.properties and persist it into instance of the class. Use annotation @ConfigurationProperties(prefix = "example")
-
Create bean to work with our application in different language:
- LocaleResolver localeResolver()
- ResourceBundleMessageSource messageSource()
- Create files messages_en.properties and messages_ru.properties in resource folder
- Inject MessageSource in LicenseService and use it
-
Add @RefreshScope in LicenseServiceApplication class.
Spring Boot Actuator offers a @RefreshScope annotation that allows a development team to access a /refresh endpoint that will force the Spring Boot application to reread its application configuration.
-
Implementing Spring HATEOAS to display related links
-
The DevOps story: Building for the rigors of runtime
- Service assembly: Packaging and deploying your microservices.
./gradlew assemble && java -jar build/libs/licensing-service-0.0.1-SNAPSHOT.jar- Create dockerfile
FROM openjdk:17 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]or using gradle
./gradlew bootBuildImage --imageName=ostock/licensing-service:0.0.1-SNAPSHOT- Pack our project in docker image and try to start
docker build --build-arg JAR_FILE=build/libs/*.jar -t ostock/licensing-service:0.0.1-SNAPSHOT .docker run --name licensing-service -p 8180:8180 ostock/licensing-service:0.0.1-SNAPSHOT- Create docker-compose.yml and try to start it
docker-compose up
-
Create default application.properties in which to specify connect to config server and active profile
-
We can compile jar with override properties using next command:
java -Dspring.cloud.config.uri=http://localhost:8071 -Dspring.profiles.active=dev -jar target/licensing-service-0.0.1-SNAPSHOT.jar -
You can check all environment variables through actuator
http://localhost:8180/actuator/env
- Add db/migration in resource and enable spring.flyway in properties
- Add ExceptionController class. You can handle exceptions globally and centrally using classes annotated with @ControllerAdvice.
- Add ErrorMessage class. Instance of that class can contain our error with code, status and message
- Add RestErrorList class. This class extend ArrayList and contains list of ErrorMessage
- Add ResponseWrapper Class to wrap RestErrorList
- Create new spring boot application (Organization service) with following dependencies:
- spring-boot-starter-actuator
- spring-cloud-starter-bootstrap
- spring-boot-starter-data-jpa
- spring-boot-starter-web
- spring-cloud-starter-config
- lombok
- postgresql
-
Create new spring boot application (config server) with following dependencies:
- Actuator
- Config server
- spring-cloud-starter-bootstrap
-
Create bootstrap.properties
-
Add @EnableConfigServer annotation in ConfigurationServerApplication class
-
Create config for licensing-service:
- licensing-service.properties
- licensing-service-dev.properties
- licensing-service-prod.properties
And put it in git repository in config folder Spring framework implements a hierarchical mechanism for properties. First applied licensing-service.properties and after then licensing-service-dev.properties
-
Create Dockerfile and edit docker-compose.yml for new image
-
Check it service is work
http://localhost:8071/licensing-service/default -
Add encrypt symmetric key in bootstrap.properties. The symmetric encryption key is nothing more than a shared secret that’s used by the encrypter to encrypt a value and by the decrypter to decrypt a value.
Now we can use http://localhost:8071/encrypt and http://localhost:8071/decrypt for our passwords
- Add new module spring boot with following dependencies:
- spring-boot-starter-web
- spring-cloud-starter-config
- spring-cloud-starter-loadbalancer
- spring-cloud-starter-netflix-eureka-server
- spring-cloud-starter-bootstrap
- Add @EnableEurekaServer annotation in EurekaServerApplication class
- Check http://localhost:8070/eureka/apps/organization-service
- Add dependencies:
- spring-cloud-starter-bootstrap
- spring-boot-starter-actuator
- spring-cloud-starter-config
- spring-cloud-starter-gateway
- spring-cloud-starter-netflix-eureka-client
- Change access to organization-service and licensing-service
- You can check new routes by address http://localhost:8072/actuator/gateway/routes
-
Create new docker container for keyclock image
-
Open localhost:8080, login and password "admin" and config new realm "spmia-realm"
-
Add frontend URL - http://keycloak:8080/auth
-
Create new client "ostock". Access Type = confidential, Service Accounts Enabled = ON, Authorization Enabled = ON
-
Create two client's roles - USER and ADMIN (not composite)
-
Create two realm's roles - ostock-admin and ostock-user (composite) relative with client's roles
-
Create new user, login = hexhoc, password = Vcsdfr13, role = ostock_admin
-
Create POST query for get access token. Basic auth - username = ostock, password = QMCX3PjFa9nrVObDSi12ISH790vWRfO4 ( client's credentials) and body - grant_type = password, username = hexhoc, password = Vcsdfr13
curl --location --request POST 'http://localhost:8080/auth/realms/spmia-realm/protocol/openid-connect/token' \ --header 'Authorization: Basic b3N0b2NrOlFNQ1gzUGpGYTluclZPYkRTaTEySVNINzkwdldSZk80' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=password' \ --data-urlencode 'username=hexhoc' \ --data-urlencode 'password=Vcsdfr13'
- Add a few dependencies:
- keycloak-spring-boot-starter
- spring-boot-starter-security
- Add configuration properties (connect, rules, etc) for KeyClock
- Create SpringConfig class that extend KeycloakWebSecurityConfigurerAdapter. This class configure organizaton-service how it should secure resource
- Add @RolesAllowed annotation in OrganizationController class
- Add a few dependencies:
- keycloak-spring-boot-starter
- spring-boot-starter-security
- Add configuration properties (connect, rules, etc) for KeyClock
- Create SpringConfig class that extend KeycloakWebSecurityConfigurerAdapter. This class configure licensing-service how it should secure resource
- Create @Bean KeycloakRestTemplate. Keycloak provides a new REST template class that support -
- Grab the HTTP header of the incoming licensing service call
- Add it to every outbound service call in the licensing service
- When we use RestTemplate to get data from another service, we should do it through gateway - http://gateway-server:8072 or http://localhost:8072 (to dev profile).
- Add dependency - json
- Add in FilterUtils class method that extract and return Auth token from headers
- Add in TrackingFilter class method getUsername that decode auth token and get Username from it
- Change routes in configuration properties. By default, the gateway doesn’t forward sensitive HTTP headers like cookie, set-cookie, and authorization to downstream services
Pay attention! we should not use any interceptors to intercept and distribute the user token. This whole key-cloak makes itself. When some service get token, it itself going to the KeyCloak service and validate it.
-
User somehow get token from KeyClock! TODO
-
Call licensing service with token through gateway
-
The gateway looks up the licensing service endpoint and then forwards the call to one of the licensing service’s servers. The services gateway copies the authorization HTTP header from the incoming call and ensures that the HTTP header is forwarded on to the new endpoint.
This is work automatically. We do not need to do something for that.
-
The licensing service receives the incoming call. Because the licensing service is a protected resource, the licensing service will validate the token with the Keycloak server and then check the user’s roles for the appropriate permissions. As part of its work, the licensing service invokes the organization service. When doing this, the licensing service needs to propagate the user’s access token to the organization service.
For production use, you should also build your microservice security around the following practices:
- Use HTTPS/Secure Sockets Layer (SSL) for all service communications.
- Use an API gateway for all service calls.
- Provide zones for your services (for example, a public API and private API).
- Limit the attack surface of your microservices by locking down unneeded network ports
In order not to contact the organization service every time to obtain an organization, we use Radis. But if there has been a change in the organization, then it is necessary to remove the old cache of the organization from the Radis.
Since there is no need to execute the operation synchronously, we will use kafka as a message broker to execute requests asynchronously.
- The organization service registers messages about any actions carried out with the organization (GET, POST, PUT, DELETE) in OrganizationServiceImpl class, using simpleSourceBean.publishOrganizationChange() method.
- The licensing service listen all incoming message from kafka and execute it. Using OrganizationChangeHandler class
Add docker images:
- kafka
- zookeeper
- redis
Modify the organization service to publish a message to Kafka every time the organization service changes data
- Add dependencies:
- spring-cloud-stream
- spring-cloud-starter-stream-kafka
- @EnableBinding(Source.class) in OrganizationServiceApplication class
- Add configuration properties
- Channel for communication
- Content-type
- Bind with kafka
- Create OrganizationChangeModel class. This class is DTO for kafka with following fields:
- type
- action
- organizationId
- correlationId
- Create SimpleSourceBean class. This class will implement publishOrganizationChange method this method get data about changes create OrganizationChangeModel instance and send it to message broker (kafka)
- Add ThreadLocal static variables to UserContext class, to store data individually for the current thread.
- Add dependencies:
- spring-cloud-stream
- spring-cloud-starter-stream-kafka
- jedis. Spring uses the Jedis open source project to communicate with a Redis server
- spring-data-redis
- Add configuration properties
- Channel for communication
- Content-type
- Bind with kafka
- Create OrganizationChangeModel class. This class is DTO for kafka with following fields:
- type
- action
- organizationId
- correlationId
- Create OrganizationChangeHandler class to logging communication with kafka
- Create CustomChannels class to custom channel for kafka
- Add ThreadLocal static variables to UserContext class, to store data individually for the current thread.
We are work with redis like database, using entity and repository for implement CRUD model.
- Add dependencies:
- jedis. Spring uses the Jedis open source project to communicate with a Redis server
- spring-data-redis
- Add configuration properties
- Redis host and port
- Create @Bean JedisConnectionFactory to connect to redis server and @Bean @RedisTemplate to using that connect to work with redis
- Create interface repository OrganizationRedisRepository. This will be crud model for redis
- Add @RedisHash("organization") to Organization class. Organization is entity that contains in redis hash
- Edit OrganizationRestTemplateClient class to check redis cache before retrieve organization from organization-service
Because microservices are distributed by nature, trying to debug where a problem occurs can be maddening. The distributed nature of the services means that we need to trace one or more transactions across multiple services, physical machines, and different data stores, and then try to piece together what exactly is going on.
- Add following dependencies in licensing, config and organization service:
- spring-cloud-starter-sleuth
- logstash-logback-encoder. By default, logback collect log in plaint text, but this library covert data to json
- Create and configure logback-spring.xml file in resource folder. In that file we describe that destination for logs will be logstash
- Add docker images:
-
Elasticsearch
-
Logstash
- Create config file logstash.conf with following plugins:
- Input. In this section, we specify the tcp plugin for consuming the log data. Next is the port number 5000 this is the port that we’ll specify for Logstash later in the docker-compose.yml file.
- Filter. We added a mutate filter. This filter adds a manningPublications tag to the events. A real-world scenario of a possible tag for your services might be the environment where the application runs
- Output. specify the output plugin for our Logstash service and send the processed data to the Elasticsearch service running on port 9200
- Create config file logstash.conf with following plugins:
-
Kibana
-
- Configuring Kibana
- Open http://localhost:5601/ and choose option "Explore on my own"
- We must create an index pattern. Kibana uses a set of index patterns to retrieve the data from an Elasticsearch engine. The index pattern is in charge of telling Kibana which Elasticsearch indexes we want to explore. Use next pattern - logstash-* i
- For step 2, we’ll specify a time filter. To do this, we need to select the @timestamp option under the Time Filter Field Name drop-down list.
- Profit. We can now start making requests to our services to see the real-time logs in Kibana.
- Searching for Spring Cloud Sleuth trace IDs in Kibana
- Make simple request to get license from service. After that logstash collect some log with specific traceId
- Open Kibana and using filter try to filter logs by traceId (for example: traceId:d86f625b10c5e70b)
- For example, we added a tag with the mutate filter in Logstash, and we can use filter by that field also.
- Add traceId in response header. If we want to analyze our request, we need to know the traceId of our request. In
this step we include traceId in response header.
- Open ResponseFilter and inject Tracer. Because the gateway is now Spring Cloud Sleuth–enabled, we can access tracing information from within our ResponseFilter by autowiring in the Tracer class
- Edit postGlobalFilter method. Get traceId from tracer and put it in headers response
- Use GET request
And after check response header "tmx-correlation-id"http://localhost:8072/licservice/v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3/license/f2a9c9d4-d2c0-44fa-97fe-724d77173c62/rest - Setting up the Spring Cloud Sleuth and Zipkin dependencies
- Add dependency spring-cloud-sleuth-zipkin to gateway, organization and licensing service
- Add spring.zipkin.baseUrl:http://zipkin:9411 in configuration files for following services: gateway, licensing and organization
- Add our own trace span. We want to trace our request to redis. Add trace span in OrganizationRestTemplateClient.checkRedisCache()
- Add our own trace span. We want to trace our request to redis.
- And now we can make request to license and after look in zipkin (http://localhost:9411/zipkin/) and compare how fast request to redid and request to db