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 ligne packageArchetype.java_application
  • il vous faudra une fichier projet/plugins.sbt avec cette ligne addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.8.0")

Et vous avez tout ce qu’il faut pour hĂ©berger vos microservices chez Clever 😉.

blog comments powered by Disqus

Related posts