Microservices avec Vert-x en Scala
Je viens de passer 2 semaines trĂšs âmicroservicesâ:
Jâai enfin eu lâoccasion de voir le talk de Quentin sur les microservices https://twitter.com/alexandrejomin/status/860443891971088384 lors de notre passage chez @Xee_FR Ă Lille (vous pouvez aussi voir aussi ceci Ă Devoxx France ProblĂšmes rencontrĂ©s en microservice (Quentin Adam) et Comment maintenir de la cohĂ©rence dans votre architecture microservices (ClĂ©ment Delafargue)).
Jâai lu lâexcellent Building Reactive Microservices in Java par @clementplop, oĂč ClĂ©ment explique comment Ă©crire des microservices en Vert-x. (Ă voir aussi: Vert.X: Microservices Were Never So Easy (Clement Escoffier)
Jâai pu assister Ă la prĂ©sentation âMODERNISEZ VOS APPLICATIONS AVEC RXJAVA ET VERT.Xâ par Thomas Segismont.
Du coup, je nâai plus le choix, il faut que je mây mette sĂ©rieusement et que je prĂ©pare quelques dĂ©mos MicroServices pour mon job. Et autant que je vous en fasse profiter. đ Jâai dĂ©cidĂ© de le faire en Scala (mon auto-formation), mais je vais tout faire pour que cela reste le plus lisible possible.
Architecture de mon exemple
â ïž note: cette âarchitectureâ est pensĂ©e pour ĂȘtre le plus simple possible Ă comprendre - cela ne signifie pas que ce soit ce quâil faut utiliser en production - lâobjectif est dâapprendre simplement. (je vais faire des microservices http) - je ne traiterais pas de des âCircuit Breakersâ, ou des âHealth Checks and Failoversâ.
Lorsque vous avez un ensemble de microservices, câest bien dâavoir un systĂšme qui permetten de rĂ©fĂ©rencer ces microservices pour facilement les âtrouverâ. Une application qui âconsommeâ un microservice doit avoir moyen de le rĂ©fĂ©rencer et lâutiliser sans pour autant connaĂźtre Ă lâavance son adresse (par ex: lâurl du microservice). On parle de âlocation transparencyâ et de pattern âservice discoveryâ. Câest Ă dire quâun microservice, doit ĂȘtre capable dâexpliquer lui-mĂȘme comment on peut lâappeler et lâutiliser et ces informations sont stockĂ©es dans une âService Discovery Infrastructureâ.
Vert.x Service Discovery
Vert.x fournit tout un ensemble dâoutils pour faire ça et se connecter Ă un service Consul, Zookeeper, ⊠Mais Vert.x fournit aussi un âDiscovery Backend - Redisâ qui vous permet dâutiliser une base Redis comme annuaire de microservices (cf. Discovery Backend with Redis). Câest ce que je vais utiliser pour mon exemple.
Donc pour résumer, je vais faire:
- microservice qui se âdĂ©clareâ au âDiscovery Backendâ
- un âconsumerâ qui va aller interroger le âDiscovery Backendâ pour obtenir une rĂ©fĂ©rence au microservice et ensuite lâutiliser
Création du microservice
Préparation
Tout dâabord nous allons crĂ©er un projet calculator-vx-service
mkdir calculator-vx-service
cd calculator-vx-service
mkdir -p src/{main,test}/{java,resources,scala}
mkdir lib project target
Créez un fichier build.sbt
Ă la racine du projet:
name := "calculator-vx-service"
version := "1.0"
scalaVersion := "2.12.2"
libraryDependencies += "io.vertx" %% "vertx-web-scala" % "3.4.1"
libraryDependencies += "io.vertx" %% "vertx-service-discovery-scala" % "3.4.1"
libraryDependencies += "io.vertx" %% "vertx-service-discovery-backend-redis-scala" % "3.4.1"
Créez un fichier project/build.properties
sbt.version = 0.13.15
Code
Ensuite créez un fichier src/main/scala/Calculator.scala
:
import io.vertx.core.json.JsonObject
import io.vertx.scala.core.Vertx
import io.vertx.scala.ext.web.Router
import io.vertx.scala.servicediscovery.types.HttpEndpoint
import io.vertx.scala.servicediscovery.{ServiceDiscovery, ServiceDiscoveryOptions}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
object Calculator {
val vertx = Vertx.vertx()
def main(args: Array[String]): Unit = {
val server = vertx.createHttpServer()
val router = Router.router(vertx)
val httpPort = sys.env.get ("PORT").getOrElse("8080").toInt
router.get("/api/add/:a/:b").handler(context => {
val res: Integer = context.request.getParam("a").get.toInt + context.request.getParam("b").get.toInt
context
.response()
.putHeader("content-type", "application/json;charset=UTF-8")
.end(new JsonObject().put("result", res).encodePrettily())
})
router.get("/api/multiply/:a/:b").handler(context => {
val res: Integer = context.request.getParam("a").get.toInt * context.request.getParam("b").get.toInt
context
.response()
.putHeader("content-type", "application/json;charset=UTF-8")
.end(new JsonObject().put("result", res).encodePrettily())
})
// home page
router.get("/").handler(context => {
context
.response()
.putHeader("content-type", "text/html;charset=UTF-8")
.end("<h1>Hello đ</h1>")
})
println(s"đ Listening on $httpPort - Enjoy đ")
server.requestHandler(router.accept _).listen(httpPort)
}
}
Si vous compilez et lancez vous avez votre microservice de calcul qui vous permet de faire des additions et des multiplications:
- http://localhost:8080/api/add/40/2
- http://localhost:8080/api/multiply/21/2
Maintenant, on souhaite que notre microservice soit âdĂ©couvrableâ
Rendre le service âdĂ©couvrableâ
Pour cela, nous allons ajouter une méthode à notre objet Calculator
def discovery = {
// Settings for the Redis backend
val redisHost = sys.env.get("REDIS_HOST").getOrElse("127.0.0.1")
val redisPort = sys.env.get("REDIS_PORT").getOrElse("6379").toInt
val redisAuth = sys.env.get("REDIS_PASSWORD").getOrElse(null)
val redisRecordsKey = sys.env.get("REDIS_RECORDS_KEY").getOrElse("scala-records")
// Mount the service discovery backend (Redis)
val discovery = ServiceDiscovery.create(vertx, ServiceDiscoveryOptions()
.setBackendConfiguration(
new JsonObject()
.put("host", redisHost)
.put("port", redisPort)
.put("auth", redisAuth)
.put("key", redisRecordsKey)
)
)
// Settings for record the service
val serviceName = sys.env.get("SERVICE_NAME").getOrElse("calculator")
val serviceHost = sys.env.get("SERVICE_HOST").getOrElse("localhost") // domain name
val servicePort = sys.env.get("SERVICE_PORT").getOrElse("8080").toInt // set to 80 on Clever Cloud
val serviceRoot = sys.env.get("SERVICE_ROOT").getOrElse("/api")
// create the microservice record
val record = HttpEndpoint.createRecord(
serviceName,
serviceHost,
servicePort,
serviceRoot
)
discovery.publishFuture(record).onComplete{
case Success(result) => println(s"đ publication OK")
case Failure(cause) => println(s"đĄ publication KO: $cause")
}
// discovery.close() // or not
}
Et vous allez appeler cette méthode discovery
dans la méthode main de Calculator
def main(args: Array[String]): Unit = {
val server = vertx.createHttpServer()
val router = Router.router(vertx)
// use redis backend to publish service informations
discovery
val httpPort = sys.env.get ("PORT").getOrElse("8080").toInt
// etc...
Pour rĂ©sumer, quâavons nous fait?
Nous créons un ServiceDiscovery
(on se connecte Ă la base Redis - que vous nâoubliez pas de lancer):
val discovery = ServiceDiscovery.create(vertx, ServiceDiscoveryOptions()
.setBackendConfiguration(
new JsonObject()
.put("host", redisHost)
.put("port", redisPort)
.put("auth", redisAuth)
.put("key", redisRecordsKey)
)
)
Nous créons un Record
qui décrit notre microservice:
// create the microservice record
val record = HttpEndpoint.createRecord(
serviceName, // calculator
serviceHost, // localhost
servicePort, // 8080
serviceRoot // /api
)
Et ensuite on publie les informations du microservice vers Redis:
discovery.publishFuture(record).onComplete{
case Success(result) => println(s"đ publication OK")
case Failure(cause) => println(s"đĄ publication KO: $cause")
}
Si vous lancez, vous pouvez vérifier que les données du microservice sont bien présentes dans la base Redis:
Maintenant, nous allons créer un consommateur de ce microservice.
Création du consommateur
Préparation
Nous allons créer un autre projet calculator-vx-invoke qui appellera les 2 opérations de notre microservice.
mkdir calculator-vx-invoke
cd calculator-vx-invoke
mkdir -p src/{main,test}/{java,resources,scala}
mkdir lib project target
Créez un fichier build.sbt
Ă la racine du projet:
name := "calculator-vx-invoke"
version := "1.0"
scalaVersion := "2.12.2"
libraryDependencies += "io.vertx" %% "vertx-web-client-scala" % "3.4.1"
libraryDependencies += "io.vertx" %% "vertx-service-discovery-scala" % "3.4.1"
libraryDependencies += "io.vertx" %% "vertx-service-discovery-backend-redis-scala" % "3.4.1"
Créez un fichier project/build.properties
sbt.version = 0.13.15
Code
Ensuite créez un fichier src/main/scala/InvokeCalculator.scala
:
import io.vertx.core.json.JsonObject
import io.vertx.scala.core.Vertx
import io.vertx.scala.ext.web.client.WebClient
import io.vertx.scala.servicediscovery.{ServiceDiscovery, ServiceDiscoveryOptions}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
object InvokeCalculator {
val vertx = Vertx.vertx()
def main(args: Array[String]): Unit = {
// Settings for the Redis backend
val redisHost = sys.env.get("REDIS_HOST").getOrElse("127.0.0.1")
val redisPort = sys.env.get("REDIS_PORT").getOrElse("6379").toInt
val redisAuth = sys.env.get("REDIS_PASSWORD").getOrElse(null)
val redisRecordsKey = sys.env.get("REDIS_RECORDS_KEY").getOrElse("scala-records")
val discoveryService = ServiceDiscovery.create(vertx, ServiceDiscoveryOptions()
.setBackendConfiguration(
new JsonObject()
.put("host", redisHost)
.put("port", redisPort)
.put("auth", redisAuth)
.put("key", redisRecordsKey)
)
)
// search service by name
discoveryService.getRecordFuture(new JsonObject().put("name", "calculator")).onComplete{
case Success(result) => {
val reference = discoveryService.getReference(result)
val client = reference.getAs(classOf[WebClient])
client.get("/api/add/40/2").sendFuture().onComplete{
case Success(result) => {
println(result.body())
}
case Failure(cause) => {
println(cause)
}
}
client.get("/api/multiply/2/21").sendFuture().onComplete{
case Success(result) => {
println(result.body())
}
case Failure(cause) => {
println(cause)
}
}
}
case Failure(cause) => {
println(cause)
}
}
}
}
Quâavons nous fait?
Nous avons une fois de plus créé un ServiceDiscovery
(on se connecte Ă la base Redis - que vous nâoubliez toujours pas de lancer):
val discoveryService = ServiceDiscovery.create(vertx, ServiceDiscoveryOptions()
Puis nous avons recherché le service par son nom avec notre discoveryService
:
discoveryService.getRecordFuture(new JsonObject().put("name", "calculator")).onComplete{...
Une fois le microservice trouvĂ©, je vais crĂ©er une rĂ©fĂ©rence Ă ce microservice Ă partir de laquelle je vais pouvoir obtenir un client qui va me permettre dâinvoquer les opĂ©rations du microservice âcalculatorâ:
val reference = discoveryService.getReference(result)
val client = reference.getAs(classOf[WebClient])
Remarque: classOf[WebClient]
car mon service est de type http.
Et maintenant si je veux faire une addition, il me suffit dâĂ©crire ceci:
client.get("/api/add/40/2").sendFuture().onComplete{
case Success(result) => {
println(result.body())
}
case Failure(cause) => {
println(cause)
}
}
Ou ceci pour une multiplication:
client.get("/api/multiply/2/21").sendFuture().onComplete{
case Success(result) => {
println(result.body())
}
case Failure(cause) => {
println(cause)
}
}
VoilĂ , vous nâavez plus quâĂ lancer pour vĂ©rifier que cela fonctionne. Ce nâest pas plus compliquĂ© que ça de faire du microservice avec Vert-x. đ
Astuce pour déployer chez Clever-Cloud
Jâai commencĂ© Ă jouer avec les microservices parceque jâai des dĂ©monstrations Ă prĂ©parer pour mon job. Du coup je vous donne juste la dĂ©marche Ă suivre pour dĂ©ployer sur Clever-Cloud.
Variables dâenvironnement
Vous devez bien sĂ»r avoir un add-on Redis qui va vous fournir les variables dâenvironnement nĂ©cessaire, comme par exemple:
REDIS_HOST yopyop-redis.services.clever-cloud.com
REDIS_PASSWORD pouyoupouyou
REDIS_PORT 3062
REDIS_URL redis://:pouyoupouyou@yopyop-redis.services.clever-cloud.com:3062
Une application web chez Clever doit Ă©couter sur le port 8080
, et de lâextĂ©rieur vous attaquerez votre microservice avec le port 80
Ce qui veut dire que quand vous allez déclarer votre microservice au backend Redis, il faudra préciser 80
comme port:
val record = HttpEndpoint.createRecord(
"calculator",
"your.domain.name",
80,
"/api"
)
Mais faire Ă©couter le service sur 8080
lorsque vous le lancez:
server.requestHandler(router.accept _).listen(8080)
Build
Il y a 2 petites choses Ă ajouter:
- dans
build.sbt
ajouter la lignepackageArchetype.java_application
- il vous faudra une fichier
projet/plugins.sbt
avec cette ligneaddSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.8.0")
Et vous avez tout ce quâil faut pour hĂ©berger vos microservices chez Clever đ.
Tweet