Lagom implementation

HeartAI services are developed with the Lagom microservices framework. Lagom provides libraries for the Scala and Java programming languages. HeartAI services are primarily developed with the Scala language. Lagom design choices provide a structured environment for developers to benefit from modern microservices software concepts, and many of the Lagom implementations are best practice for reactive microservices architectures.

Lagom provides best practice constructs for:

Although Lagom is relatively opinionated as a framework, the native implementation of Akka and Play allows for diverse flexibility and extensibility.

Service architecture

HeartAI system services implement reactive microservices architectures and follow concepts from The Reactive Manifesto. Many architectural concepts of the system may be considered as reactive design patterns and event-driven architectures. The overall composition of these services creates the service-level application software of the system. Further information about HeartAI system services may be found with the following documentation:

Lagom service configuration

HeartAI implements service-level configuration for Lagom using the Typesafe Config, a configuration library for JVM languages that is specifiable with the HOCON format. Typesafe config configuration files are loaded at the time of JVM initialisation, but where appropriate JVM properties may also be modified at runtime.

Typesafe config configuration files follow the MAVEN Standard Directory Layout, and may usually be found at:

src/main/resources/
src/test/resources/

Example: Lagom service configuration - Local-machine development environment

The following example shows a Typesafe config configuration file for HeartAI HelloWorldService local-machine development environments. In particular, note that the PostgreSQL driver configuration refers to a PostgreSQL instance that is present on localhost, which is provided as part of the HeartAI development environment.

# HeartAI
heartai.local_mode = true
heartai.service_topic.greeting_messages_changed = "hello_world_greeting_messages_changed_topic_dev"

# Play
play.application.loader = net.heartai.hello_world.HelloWorldLoader

# Akka actor
akka.actor {
  serializers {
    jackson-json = "akka.serialization.jackson.JacksonJsonSerializer"
  }
  serialization-bindings {
    "net.heartai.hello_world.JSONSerialisable" = jackson-json
    "net.heartai.hello_world.CBORSerialisable" = jackson-cbor
  }
}

# Akka cluster
akka.cluster {
  shutdown-after-unsuccessful-join-seed-nodes = 60s
}

# Akka remote
akka.remote {
  artery {
    enable = on
    transport = tcp
    bind.hostname = "0.0.0.0"
    //    bind.port = 2552
    //    canonical.port = 2552
  }
}

# Akka management
akka.management {
  http {
    bind-hostname = "0.0.0.0"
    port = 2552
    bind-port = 2552
  }
}

# PostgreSQL
db.default {
  driver = "org.postgresql.Driver"
  url = "jdbc:postgresql://postgres:5432/heartai"
  username = heartai
  password = heartai
}
hikaricp {
  minimumIdle = 5
  maximumPoolSize = 10
}
jdbc-defaults.slick {
  profile = "slick.jdbc.PostgresProfile$"
}

# Lagom
lagom.cluster {
  exit-jvm-when-system-terminated = on
}
lagom.persistence.jdbc {
  create-tables {
    auto = true
    timeout = 20s
    failure-exponential-backoff {
      min = 3s
      max = 30s
      random-factor = 0.2
    }
  }
}

# Circuit breakers for calls to other services are configured
# in this section. A child configuration section with the same
# name as the circuit breaker identifier will be used, with fallback
# to the `lagom.circuit-breaker.default` section.
lagom.circuit-breaker {

  # Default configuration that is used if a configuration section
  # with the circuit breaker identifier is not defined.
  default {
    # Possibility to disable a given circuit breaker.
    enabled = on

    # Number of failures before opening the circuit.
    max-failures = 20

    # Duration of time after which to consider a call a failure.
    call-timeout = 20s

    # Duration of time in open state after which to attempt to close
    # the circuit, by first entering the half-open state.
    reset-timeout = 15s

    # A whitelist of fqcn of Exceptions that the CircuitBreaker
    # should not consider failures. By default all exceptions are
    # considered failures.
    exception-whitelist = []
  }
}

# Keycloak
keycloak.service_group = "hello-world-service"

Example: Lagom service configuration - Cluster production environment

The following example shows a Typesafe config configuration file for HeartAI HelloWorldService production environments. Note that this configuration includes the following declaration:

include "application"

which injects the application.conf local-machine development environment configurations. In this instance, if a production environment configuration file declares the same configuration parameter, the production environment configuration takes precedence as it is declared later in the configuration file. This allows the production.conf configuration values to inherit application.conf configuration values where these are shared across local-machine development environments and cluster-based production environments.

Configuration declarations of the following type:

# Akka remote
akka.remote {
  artery {
    enable = on
    transport = tcp
    bind.hostname = ${HTTP_BIND_ADDRESS}
    bind.port = ${AKKA_REMOTING_PORT}
    canonical.port = ${AKKA_REMOTING_PORT}
  }
}

search the local process context for environment variables, in this case for the environment variables named HTTP_BIND_ADDRESS and AKKA_REMOTING_PORT. This allows the injection of configuration values through mechanisms such as Kubernetes / OpenShift Pod specifications, for example as described later in the documentation section Service deployment: OpenShift Deployment.

include "application"

# HeartAI
heartai.local_mode = false

# Akka cluster
akka.discovery {
  method = akka-dns
}
coordinated-shutdown.exit-jvm = on
cluster {
  shutdown-after-unsuccessful-join-seed-nodes = 120s
}

# Akka remote
akka.remote {
  artery {
    enable = on
    transport = tcp
    bind.hostname = ${HTTP_BIND_ADDRESS}
    bind.port = ${AKKA_REMOTING_PORT}
    canonical.port = ${AKKA_REMOTING_PORT}
  }
}

# Akka management
akka.management {
  cluster.bootstrap {
    contact-point-discovery {
      discovery-method = kubernetes-api
      service-name = ${AKKA_DISCOVERY_SERVICE_NAME}
      required-contact-point-nr = ${REQUIRED_CONTACT_POINT_NR}
    }
  }

  http {
    bind-hostname = ${HTTP_BIND_ADDRESS}
    port = ${AKKA_MANAGEMENT_PORT}
    bind-port = ${AKKA_MANAGEMENT_PORT}
  }
}

# Play configuration
play {
  server {
    pidfile.path = "/dev/null"
    http.port = ${HTTP_PORT}
    https.port = ${HTTPS_PORT}
  }
  http.secret.key = "${application_secret}"
  https.secret.key = "${application_secret}"
}

# Lagom
lagom.broker.kafka {
  service-name = ""
  brokers = ${?KAFKA_BOOTSTRAP_SERVICE}
}

lagom.persistence.ask-timeout = 30s
lagom.persistence.call-timeout = 30s
lagom.persistence.jdbc.create-tables.auto = false

# PostgreSQL
db.default {
  driver = "org.postgresql.Driver"
  url = ${POSTGRESQL_CONTACT_POINT}
  username = ${POSTGRESQL_USERNAME}
  password = ${POSTGRESQL_PASSWORD}
}

Lagom service endpoint application-programming interface

Lagom suggests an architectural design where the software components of service endpoint application-programming interfaces (APIs) are specified separately from corresponding service endpoint implementations. This allows API-specific components to be declared and managed independently of their corresponding implementation details. A typical Lagom-designed API has declarations for service dependencies, service resources, service descriptors, and service class definitions. End-users and developers should consult the service API as a manifest of service functionalities and communication protocols, and design corresponding service clients with the service API as a contract for how to interface with the service.

Service interfaces

HeartAI system services provides abstractions for declaring service application-programming interfaces (APIs) which service clients may use as a manifest of service functionalities and communication protocols. Further information about interfacing with HeartAI system services may be found with the following documentation:

Example: Lagom service endpoint application-programming interface - Service dependencies

The following example for a Lagom-designed service endpoint API shows the service dependencies for the HeartAI HelloWorldService service:


import akka.Done import akka.NotUsed import com.lightbend.lagom.scaladsl.api.broker.Topic import com.lightbend.lagom.scaladsl.api.broker.kafka.KafkaProperties import com.lightbend.lagom.scaladsl.api.broker.kafka.PartitionKeyStrategy import com.lightbend.lagom.scaladsl.api.transport.Method import com.lightbend.lagom.scaladsl.api.Descriptor import com.lightbend.lagom.scaladsl.api.Service import com.lightbend.lagom.scaladsl.api.ServiceCall import com.typesafe.config.ConfigFactory import net.heartai.core.PingServiceAPI import play.api.libs.json.Format import play.api.libs.json.Json import java.util.UUID

The corresponding libraries are:

Library Description Reference
Akka Akka actor system https://akka.io
Lagom Scala DSL Lagom implementation with Scala https://www.lagomframework.com/documentation/latest/scala/Home.html
Typesafe Config Typesafe JVM configuration library https://github.com/lightbend/config
HeartAI PingService Service functionality for ping endpoints
Play JSON Play package for JSON management https://www.playframework.com/documentation/2.8.x/ScalaJson
Java Utils Java utilities package https://docs.oracle.com/javase/8/docs/api/java/util/package-summary.html

Example: Lagom service endpoint application-programming interface - Endpoint resources

Lagom provides ServiceCall and ServerServiceCall traits to implement HTTP-based service endpoint resources provided with the Play Framework.

Examples of ServiceCall for the HelloWorldService are shown with the following:

Service endpoint implementation resources

The corresponding service endpoint implementation resources to this example service endpoint API resources may be found at the following documentation section:

def helloPublic(
  id: String):
ServiceCall[NotUsed, Greeting]

def helloSecure(
  id: String):
ServiceCall[NotUsed, Greeting]

def updateGreetingMessage(
  id: String):
ServiceCall[GreetingMessage, Done]

These endpoint resources provide the following functionalities:

Service endpoint resource Functionality
helloPublic() Returns a Greeting message corresponding to the id of the resource. Each id has an individual Greeting message, with the default message being "Hello".
helloSecure() Provides the same functionality as helloPublic(), but also requires a secure access token to be presented to the service endpoint.
updateGreetingMessage() Allows the Greeting message corresponding to the id to be updated. Future invocations of helloPublic or helloSecure will return a Greeting with the updated message.

Example: Lagom service endpoint application-programming interface - Brokered message bus topics

Lagom also natively supports Topic traits to broker with a corresponding message bus, with integrated support for Apache Kafka.

Examples of Topic for the HelloWorldService are shown with the following:

def greetingUpdatedTopic():
Topic[Greeting]

These brokered topics provide the following functionalities:

Service brokered topic Functionality
greetingUpdatedTopic() Service broker to Apache Kafka message bus endpoint, allowing service entity generated GreetingMessageUpdatedEvent to be published to the message bus endpoint

Example: Lagom service endpoint application-programming interface - Descriptor

Lagom provides abstractions for specifying service interface endpoints through the use of service descriptors. The Lagom Descriptor trait allows the specification of service endpoint resource pathing and provides automated methods for generating an access control list.

The Descriptor implementation for the example HelloWorldService is shown following:

override final def descriptor: Descriptor = {
  import Service._
  named("hello-world")
    .withCalls(
      restCall(Method.GET, "/hello/api/ping", pingService()),
      restCall(Method.POST, "/hello/api/ping", pingServiceByPOST()),
      restCall(Method.GET, "/hello/api/ping_ws_count", pingServiceByWebSocketCount),
      restCall(Method.GET, "/hello/api/ping_ws_echo", pingServiceByWebSocketEcho),
      restCall(Method.GET, "/hello/api/hello/:id", this.helloPublic _),
      restCall(Method.GET, "/hello/api/secure/hello/:id", this.helloSecure _),
      restCall(Method.POST, "/hello/api/secure/greeting/:id", this.updateGreetingMessage _))
    .withTopics(
      topic(HelloWorldServiceAPI.GREETING_MESSAGES_CHANGED_TOPIC, greetingUpdatedTopic _)
        .addProperty(
          KafkaProperties.partitionKeyStrategy,
          PartitionKeyStrategy[Greeting](_.id)))
    .withAutoAcl(true)
}

Example: Lagom service endpoint application-programming interface - Full declaration

The full declaration for the HelloWorldService service endpoint application-programming interface is shown with the following:

package net.heartai.hello_world


import akka.Done
import akka.NotUsed
import com.lightbend.lagom.scaladsl.api.broker.Topic
import com.lightbend.lagom.scaladsl.api.broker.kafka.KafkaProperties
import com.lightbend.lagom.scaladsl.api.broker.kafka.PartitionKeyStrategy
import com.lightbend.lagom.scaladsl.api.transport.Method
import com.lightbend.lagom.scaladsl.api.Descriptor
import com.lightbend.lagom.scaladsl.api.Service
import com.lightbend.lagom.scaladsl.api.ServiceCall
import com.typesafe.config.ConfigFactory
import net.heartai.core.PingServiceAPI
import play.api.libs.json.Format
import play.api.libs.json.Json
import java.util.UUID

object HelloWorldServiceAPI {
  val config: com.typesafe.config.Config =
    ConfigFactory.load()
  lazy val GREETING_MESSAGES_CHANGED_TOPIC: String =
    config.getString("heartai.service_topic.greeting_messages_changed")
}

trait HelloWorldServiceAPI
  extends Service
    with PingServiceAPI {

  def helloPublic(
    id: String):
  ServiceCall[NotUsed, Greeting]

  def helloSecure(
    id: String):
  ServiceCall[NotUsed, Greeting]

  def updateGreetingMessage(
    id: String):
  ServiceCall[GreetingMessage, Done]

  def greetingUpdatedTopic():
  Topic[Greeting]

  override final def descriptor: Descriptor = {
    import Service._
    named("hello-world")
      .withCalls(
        restCall(Method.GET, "/hello/api/ping", pingService()),
        restCall(Method.POST, "/hello/api/ping", pingServiceByPOST()),
        restCall(Method.GET, "/hello/api/ping_ws_count", pingServiceByWebSocketCount),
        restCall(Method.GET, "/hello/api/ping_ws_echo", pingServiceByWebSocketEcho),
        restCall(Method.GET, "/hello/api/hello/:id", this.helloPublic _),
        restCall(Method.GET, "/hello/api/secure/hello/:id", this.helloSecure _),
        restCall(Method.POST, "/hello/api/secure/greeting/:id", this.updateGreetingMessage _))
      .withTopics(
        topic(HelloWorldServiceAPI.GREETING_MESSAGES_CHANGED_TOPIC, greetingUpdatedTopic _)
          .addProperty(
            KafkaProperties.partitionKeyStrategy,
            PartitionKeyStrategy[Greeting](_.id)))
      .withAutoAcl(true)
  }
}

case class GreetingMessage(
  message: String)

object GreetingMessage {
  implicit val format:
    Format[GreetingMessage] =
    Json.format[GreetingMessage]
}

case class Greeting(
  id: String,
  message: String)

object Greeting {
  implicit val format:
    Format[Greeting] =
    Json.format[Greeting]
}

Lagom service endpoint implementation

The service implementation for a Lagom-designed service declares the primary implementation components of the service. The responsibilities of the service implementation typically include the internal declaration of service endpoint resources, approaches for coordinating with a corresponding service domain entity, and interface mechanisms for read-side repositories and service-brokered message buses.

Example: Lagom service endpoint implementation - Service dependencies

The following example shows the service dependencies for the HeartAI HelloWorldService service application-programming interface:


import akka.Done import akka.NotUsed import akka.actor.ActorSystem import akka.cluster.sharding.typed.scaladsl.ClusterSharding import akka.cluster.sharding.typed.scaladsl.EntityRef import akka.management.scaladsl.AkkaManagement import akka.pattern.StatusReply import akka.stream.Materializer import akka.util.Timeout import com.lightbend.lagom.scaladsl.api.ServiceCall import com.lightbend.lagom.scaladsl.api.broker.Topic import com.lightbend.lagom.scaladsl.api.transport.BadRequest import com.lightbend.lagom.scaladsl.api.transport.ResponseHeader import com.lightbend.lagom.scaladsl.broker.TopicProducer import com.lightbend.lagom.scaladsl.persistence.EventStreamElement import com.lightbend.lagom.scaladsl.persistence.PersistentEntityRegistry import com.lightbend.lagom.scaladsl.persistence.ReadSide import com.lightbend.lagom.scaladsl.server.ServerServiceCall import com.typesafe.config.ConfigFactory import net.heartai.core.PingServiceIMPL import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer.requireAnyRole import org.pac4j.core.config.Config import org.pac4j.core.profile.CommonProfile import org.pac4j.lagom.scaladsl.SecuredService import org.slf4j.Logger import org.slf4j.LoggerFactory import slick.jdbc.JdbcBackend.Database import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.duration._

The corresponding libraries are:

Library Description Reference
Akka Akka actor system https://akka.io
Lagom Scala DSL Lagom implementation with Scala https://www.lagomframework.com/documentation/latest/scala/Home.html
Typesafe Config Typesafe JVM configuration library https://github.com/lightbend/config
HeartAI PingService Service functionality for ping endpoints
pac4j Lagom Security library for Lagom https://github.com/pac4j/lagom-pac4j
SLF4J Logging facade for Java http://www.slf4j.org/
Slick Slick JDBC functional-relational mapping https://scala-slick.org/
Scala concurrent Scala standard library concurrency components https://www.scala-lang.org/api/2.13.6/scala/concurrent/index.html

Example: Lagom service endpoint implementation - Service endpoint resources

Following from the design of separation of resource implementation from the corresponding API declaration, Lagom service implementations define the methods of endpoint resources. The service endpoint resources typically communicate with a corresponding service domain entity.

Service entity

The corresponding service entity to this example service implementation may be found at the following documentation section:

By message passing to these domain entities, service endpoint resources effectively function as task schedulers. Service endpoint resources are able to locate corresponding domain entities through the Lagom integrated instance of Akka Cluster. The corresponding domain entity is referenced by the Akka Cluster EntityRef. Akka Cluster provides internal name resolution for service entities across a distributed and sharded software-defined network (SDN), with message communication and serialisation that is managed at the SDN transport-level with Akka Artery Remoting.

Service endpoint API resources

The corresponding service endpoint API resources to this example service implementation may be found at the following documentation section:

def askHello(
  id: String): NotUsed => Future[Greeting] = {
  (_: NotUsed) =>
    entityRef(id)
      .ask[StatusReply[GreetingIMPL]](
        replyTo => GreetingCommand(id, replyTo))
      .map(_.getValue.msg)
      .map(message =>
        Greeting(
          id = id,
          message = message))
}

override def helloPublic(
  id: String):
ServiceCall[NotUsed, Greeting] =
  ServiceCall {
    askHello(id)
  }

override def helloSecure(
  id: String):
ServiceCall[NotUsed, Greeting] =
  authorize(
    requireAnyRole[CommonProfile](keycloakAuthGroup), (_: CommonProfile) =>
      ServerServiceCall {
        (requestHeader, _: NotUsed) =>
          val response: Future[Greeting] =
            entityRef(id)
              .ask[StatusReply[GreetingIMPL]](
                replyTo => GreetingCommand(id, replyTo))
              .map(_.getValue.msg)
              .map(message =>
                Greeting(
                  id = id,
                  message = message))
          response
            .map(res =>
              (ResponseHeader.Ok, res))
      })

override def updateGreetingMessage(
  id: String):
ServiceCall[GreetingMessage, Done] =
  authorize(
    requireAnyRole[CommonProfile](keycloakAuthGroup), (_: CommonProfile) =>
      ServerServiceCall {
        request: GreetingMessage =>
          val ref = entityRef(id)
          ref
            .ask[StatusReply[Done]](
              replyTo =>
                UpdateGreetingMessageCommand(
                  request.message,
                  replyTo))
            .map(_.getValue)
      })

Example: Lagom service endpoint implementation - Brokered message bus topics

The following example shows the Lagom service implementation of brokering with a corresponding message bus for the HeartAI HelloWorldService:

override def greetingUpdatedTopic(): Topic[Greeting] =
  TopicProducer.taggedStreamWithOffset(HelloWorldEvent.Tag) {
    (tag, fromOffset) =>
      persistentEntityRegistry
        .eventStream(tag, fromOffset)
        .map(ev => (processEvent(ev), ev.offset))
  }

private def processEvent(
  helloWorldEvent: EventStreamElement[HelloWorldEvent]
): Greeting = {
  helloWorldEvent.event match {
    case _ =>
      Greeting(
        helloWorldEvent.entityId, "HelloWorld")
  }
}

Example: Lagom service endpoint implementation - Full declaration

class HelloWorldServiceIMPL(
  system: ActorSystem,
  clusterSharding: ClusterSharding,
  persistentEntityRegistry: PersistentEntityRegistry,
  database: Database,
  readSide: ReadSide,
  reportRepository: HelloWorldReadSideRepository,
  override val securityConfig: Config
)(implicit ec: ExecutionContext)
  extends HelloWorldServiceAPI
    with SecuredService
    with PingServiceIMPL {

  AkkaManagement
    .get(system)
    .start()

  implicit val timeout:
    Timeout =
    Timeout(30.seconds)

  implicit val materializer:
    Materializer =
    Materializer(system)

  private final implicit val log: Logger =
    LoggerFactory.getLogger(classOf[HelloWorldServiceIMPL])

  private def entityRef(
    id: String):
  EntityRef[HelloWorldCommand] =
    clusterSharding.entityRefFor(HelloWorldState.typeKey, id)

  val config: com.typesafe.config.Config =
    ConfigFactory.load("application.conf")
  val keycloakAuthGroup: String =
    config.getString("keycloak.service_group")

  def envAuth[Request, Response](
    serviceCall: CommonProfile => ServerServiceCall[Request, Response]): ServerServiceCall[Request, Response] = {
    if (System.getProperty("heartai.services.testing") == "true")
      serviceCall.apply(new CommonProfile())
    else
      authorize(requireAnyRole[CommonProfile](keycloakAuthGroup), serviceCall)
  }

  def devEndpoint[Request, Response](
    serviceCall: ServerServiceCall[Request, Response]): ServerServiceCall[Request, Response] = {
    if (config.getString("heartai.local_mode") == "true")
      serviceCall
    else
      throw BadRequest("Development endpoints not available in production environment.")
  }

  def askHello(
    id: String): NotUsed => Future[Greeting] = {
    (_: NotUsed) =>
      entityRef(id)
        .ask[StatusReply[GreetingIMPL]](
          replyTo => GreetingCommand(id, replyTo))
        .map(_.getValue.msg)
        .map(message =>
          Greeting(
            id = id,
            message = message))
  }

  override def helloPublic(
    id: String):
  ServiceCall[NotUsed, Greeting] =
    ServiceCall {
      askHello(id)
    }

  override def helloSecure(
    id: String):
  ServiceCall[NotUsed, Greeting] =
    authorize(
      requireAnyRole[CommonProfile](keycloakAuthGroup), (_: CommonProfile) =>
        ServerServiceCall {
          (requestHeader, _: NotUsed) =>
            val response: Future[Greeting] =
              entityRef(id)
                .ask[StatusReply[GreetingIMPL]](
                  replyTo => GreetingCommand(id, replyTo))
                .map(_.getValue.msg)
                .map(message =>
                  Greeting(
                    id = id,
                    message = message))
            response
              .map(res =>
                (ResponseHeader.Ok, res))
        })

  override def updateGreetingMessage(
    id: String):
  ServiceCall[GreetingMessage, Done] =
    authorize(
      requireAnyRole[CommonProfile](keycloakAuthGroup), (_: CommonProfile) =>
        ServerServiceCall {
          request: GreetingMessage =>
            val ref = entityRef(id)
            ref
              .ask[StatusReply[Done]](
                replyTo =>
                  UpdateGreetingMessageCommand(
                    request.message,
                    replyTo))
              .map(_.getValue)
        })

  override def greetingUpdatedTopic(): Topic[Greeting] =
    TopicProducer.taggedStreamWithOffset(HelloWorldEvent.Tag) {
      (tag, fromOffset) =>
        persistentEntityRegistry
          .eventStream(tag, fromOffset)
          .map(ev => (processEvent(ev), ev.offset))
    }

  private def processEvent(
    helloWorldEvent: EventStreamElement[HelloWorldEvent]
  ): Greeting = {
    helloWorldEvent.event match {
      case _ =>
        Greeting(
          helloWorldEvent.entityId, "HelloWorld")
    }
  }
}

Lagom service entity

Lagom provides implementations to define a service domain entity that represents the bounded context of the service, with Akka Cluster EntityRef instances corresponding to service entity aggregate roots. Lagom provides these capabilities by abstracting Akka Persistence support for persistent event-sourced behaviour.

External references

Further documentation about the Lagom implementation of service domain entity architecture may be found with the following external references:

Example: Lagom service entity - Entity commands

The following example shows the service entity Commands for the HeartAI HelloWorldService:

trait JSONSerialisable

trait CBORSerialisable

final case class GreetingIMPL(
  msg: String)

object GreetingIMPL {
  implicit val format: Format[GreetingIMPL] =
    Json.format
}

sealed trait HelloWorldCommand
  extends JSONSerialisable

case class GreetingCommand(
  name: String,
  replyTo: ActorRef[StatusReply[GreetingIMPL]])
  extends HelloWorldCommand

case class UpdateGreetingMessageCommand(
  msg: String,
  replyTo: ActorRef[StatusReply[Done]])
  extends HelloWorldCommand

These service entity Commands have the following functionality:

Service entity command Functionality
GreetingCommand Triggers the entity to respond with a GreetingIMPL, using the active greeting message of the entity.
UpdateGreetingMessageCommand Triggers the entity to update its active greeting message. Future GreetingCommands will respond with the updated greeting message.

Example: Lagom service entity - Entity events

The following example shows the service entity Events for the HeartAI HelloWorldService:

sealed trait HelloWorldEvent
  extends AggregateEvent[HelloWorldEvent] {
  override def aggregateTag: AggregateEventTagger[HelloWorldEvent] =
    HelloWorldEvent.Tag
}

object HelloWorldEvent {
  val nShards:
    Int = 10
  val Tag: AggregateEventShards[HelloWorldEvent] =
    AggregateEventTag.sharded[HelloWorldEvent](
      numShards = nShards)
}

case class GreetingMessageUpdatedEvent(
  message: String)
  extends HelloWorldEvent

object GreetingMessageUpdatedEvent {
  implicit val format: Format[GreetingMessageUpdatedEvent] =
    Json.format
}

These service entity Eventss have the following functionality:

Service entity event Functionality
GreetingMessageUpdatedEvent Generated when the UpdateGreetingMessageCommand successfully updates the entity greeting message.

Example: Lagom service entity - Entity state

The following example shows the service entity State for the HeartAI HelloWorldService:

case class HelloWorldState(
  msg: String,
  timestamp: Instant) {

  def applyCommand(
    cmd: HelloWorldCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    cmd match {
      case cmd: GreetingCommand =>
        onGreetingCommand(cmd)
      case cmd: UpdateGreetingMessageCommand =>
        onUpdateGreetingMessageCommand(cmd)
    }

  private def onGreetingCommand(
    cmd: GreetingCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    Effect.reply(cmd.replyTo)(
      StatusReply.success(
        GreetingIMPL(s"$msg, ${cmd.name}!")))

  private def onUpdateGreetingMessageCommand(
    cmd: UpdateGreetingMessageCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    Effect
      .persist(
        GreetingMessageUpdatedEvent(
          cmd.msg))
      .thenReply(cmd.replyTo) {
        _ => StatusReply.Ack
      }

  def applyEvent(
    evt: HelloWorldEvent):
  HelloWorldState =
    evt match {
      case thisEvt: GreetingMessageUpdatedEvent =>
        onGreetingMessageUpdatedEvent(thisEvt)
      case _ =>
        this
    }

  private def onGreetingMessageUpdatedEvent(
    evt: GreetingMessageUpdatedEvent): HelloWorldState =
    copy(evt.message, Instant.now())
}

object HelloWorldState {

  val typeKey: EntityTypeKey[HelloWorldCommand] =
    EntityTypeKey[HelloWorldCommand]("HelloWorld")

  def initial: HelloWorldState =
    HelloWorldState(
      msg = "Hello",
      timestamp = Instant.now())

  implicit val format: Format[HelloWorldState] = Json.format
}

Example: Lagom service entity - Event-sourcing processes

The following example shows the GreetingCommand event-sourcing process for the HeartAI HelloWorldService:

heartai-hello-world-service-greeting-command-process.svg

The following example shows the UpdateGreetingMessageCommand event-sourcing process for the HeartAI HelloWorldService:

heartai-hello-world-service-update-greeting-message-command-process.svg

Example: Lagom service entity - Full declaration

The following example shows the full declaration of the Lagom service entity for the HeartAI HelloWorldService:

package net.heartai.hello_world

import akka.Done
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.cluster.sharding.typed.scaladsl._
import akka.pattern.StatusReply
import akka.persistence.typed.PersistenceId
import akka.persistence.typed.scaladsl.Effect
import akka.persistence.typed.scaladsl.EventSourcedBehavior
import akka.persistence.typed.scaladsl.ReplyEffect
import com.lightbend.lagom.scaladsl.persistence.AggregateEvent
import com.lightbend.lagom.scaladsl.persistence.AggregateEventShards
import com.lightbend.lagom.scaladsl.persistence.AggregateEventTag
import com.lightbend.lagom.scaladsl.persistence.AggregateEventTagger
import com.lightbend.lagom.scaladsl.persistence.AkkaTaggerAdapter
import com.lightbend.lagom.scaladsl.playjson.JsonSerializer
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
import play.api.libs.json.Format
import play.api.libs.json.Json

import java.time.Instant
import scala.collection.immutable.Seq

trait JSONSerialisable

trait CBORSerialisable

final case class GreetingIMPL(
  msg: String)

object GreetingIMPL {
  implicit val format: Format[GreetingIMPL] =
    Json.format
}

sealed trait HelloWorldCommand
  extends JSONSerialisable

case class GreetingCommand(
  name: String,
  replyTo: ActorRef[StatusReply[GreetingIMPL]])
  extends HelloWorldCommand

case class UpdateGreetingMessageCommand(
  msg: String,
  replyTo: ActorRef[StatusReply[Done]])
  extends HelloWorldCommand

sealed trait HelloWorldEvent
  extends AggregateEvent[HelloWorldEvent] {
  override def aggregateTag: AggregateEventTagger[HelloWorldEvent] =
    HelloWorldEvent.Tag
}

object HelloWorldEvent {
  val nShards:
    Int = 10
  val Tag: AggregateEventShards[HelloWorldEvent] =
    AggregateEventTag.sharded[HelloWorldEvent](
      numShards = nShards)
}

case class GreetingMessageUpdatedEvent(
  message: String)
  extends HelloWorldEvent

object GreetingMessageUpdatedEvent {
  implicit val format: Format[GreetingMessageUpdatedEvent] =
    Json.format
}

object HelloWorldEntity {

  def create(
    entityContext: EntityContext[HelloWorldCommand]):
  Behavior[HelloWorldCommand] = {
    val persistenceId:
      PersistenceId =
      PersistenceId(entityContext.entityTypeKey.name, entityContext.entityId)

    create(persistenceId)
      .withTagger(
        AkkaTaggerAdapter.fromLagom(entityContext, HelloWorldEvent.Tag))
  }

  def create(
    persistenceID: PersistenceId):
  EventSourcedBehavior[HelloWorldCommand, HelloWorldEvent, HelloWorldState] = {
    EventSourcedBehavior
      .withEnforcedReplies[HelloWorldCommand, HelloWorldEvent, HelloWorldState](
        persistenceId = persistenceID,
        emptyState = HelloWorldState.initial,
        commandHandler = (entity, cmd) =>
          entity.applyCommand(cmd),
        eventHandler = (entity, evt) =>
          entity.applyEvent(evt))
  }
}

case class HelloWorldState(
  msg: String,
  timestamp: Instant) {

  def applyCommand(
    cmd: HelloWorldCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    cmd match {
      case cmd: GreetingCommand =>
        onGreetingCommand(cmd)
      case cmd: UpdateGreetingMessageCommand =>
        onUpdateGreetingMessageCommand(cmd)
    }

  private def onGreetingCommand(
    cmd: GreetingCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    Effect.reply(cmd.replyTo)(
      StatusReply.success(
        GreetingIMPL(s"$msg, ${cmd.name}!")))

  private def onUpdateGreetingMessageCommand(
    cmd: UpdateGreetingMessageCommand):
  ReplyEffect[HelloWorldEvent, HelloWorldState] =
    Effect
      .persist(
        GreetingMessageUpdatedEvent(
          cmd.msg))
      .thenReply(cmd.replyTo) {
        _ => StatusReply.Ack
      }

  def applyEvent(
    evt: HelloWorldEvent):
  HelloWorldState =
    evt match {
      case thisEvt: GreetingMessageUpdatedEvent =>
        onGreetingMessageUpdatedEvent(thisEvt)
      case _ =>
        this
    }

  private def onGreetingMessageUpdatedEvent(
    evt: GreetingMessageUpdatedEvent): HelloWorldState =
    copy(evt.message, Instant.now())
}

object HelloWorldState {

  val typeKey: EntityTypeKey[HelloWorldCommand] =
    EntityTypeKey[HelloWorldCommand]("HelloWorld")

  def initial: HelloWorldState =
    HelloWorldState(
      msg = "Hello",
      timestamp = Instant.now())

  implicit val format: Format[HelloWorldState] = Json.format
}

object HelloWorldSerialiserRegistry extends JsonSerializerRegistry {
  override def serializers: Seq[JsonSerializer[_]] =
    Seq(
      JsonSerializer[GreetingIMPL],
      JsonSerializer[GreetingMessageUpdatedEvent],
      JsonSerializer[HelloWorldState])
}

Lagom service read-side processor

Example: Lagom service read-side processor - Class declaration

The following shows the Lagom read-side processor class declaration for the HeartAI HelloWorldService:

class HelloWorldReadSideProcessor(
  readSide: SlickReadSide,
  repository: HelloWorldReadSideRepository
) extends ReadSideProcessor[HelloWorldEvent] {

  override def buildHandler():
  ReadSideProcessor.ReadSideHandler[HelloWorldEvent] =
    readSide
      .builder[HelloWorldEvent]("hello_world")
      .setGlobalPrepare(repository.createTable())
      .setEventHandler[GreetingMessageUpdatedEvent] {
        evt =>
          repository.generateDatabaseEntry(
            Greeting(
              id = evt.entityId,
              message = evt.event.message))
      }
      .build()

  override def aggregateTags:
  Set[AggregateEventTag[HelloWorldEvent]] =
    HelloWorldEvent.Tag.allTags
}

This read-side processor triggers the following functionality:

Service entity event Triggered read-side repository method Functionality
GreetingMessageUpdatedEvent generateDatabaseEntry() Inserts or updates corresponding read-side repository entry.

The following figure shows the read-side repository process for the HeartAI HelloWorldService. Note the event-driven behaviour following from the event-sourced creation of a GreetingMessageUpdatedEvent:

heartai-hello-world-service-read-side-repository-process.svg

Example: Lagom service read-side processor - Full declaration

The following example shows the full declaration of the read-side processor for the HeartAI HelloWorldService:

package net.heartai.hello_world

import com.lightbend.lagom.scaladsl.persistence.AggregateEventTag
import com.lightbend.lagom.scaladsl.persistence.ReadSideProcessor
import com.lightbend.lagom.scaladsl.persistence.slick.SlickReadSide

class HelloWorldReadSideProcessor(
  readSide: SlickReadSide,
  repository: HelloWorldReadSideRepository
) extends ReadSideProcessor[HelloWorldEvent] {

  override def buildHandler():
  ReadSideProcessor.ReadSideHandler[HelloWorldEvent] =
    readSide
      .builder[HelloWorldEvent]("hello_world")
      .setGlobalPrepare(repository.createTable())
      .setEventHandler[GreetingMessageUpdatedEvent] {
        evt =>
          repository.generateDatabaseEntry(
            Greeting(
              id = evt.entityId,
              message = evt.event.message))
      }
      .build()

  override def aggregateTags:
  Set[AggregateEventTag[HelloWorldEvent]] =
    HelloWorldEvent.Tag.allTags
}

Lagom service read-side repository

Example: Lagom service read-side repository - Table declaration

The following example shows a declaration of a Slick Table implementation for the HeartAI HelloWorldService. Note how the two columns in this example table, id and message, are mapped onto corresponding class methods of HelloWorldTable. In addition, the combined tuple of id and message together are mapped onto Greeting case classes. These mappings are examples of functional projections of the backing relational data system onto the service-level type system.

class HelloWorldTable(
  tag: Tag
) extends Table[Greeting](tag, "hello_world") {

  def id:
  Rep[String] =
    column[String]("id", O.PrimaryKey)

  def message:
  Rep[String] =
    column[String]("string")

  def * :
  ProvenShape[Greeting] =
    (id, message) <>
      ((Greeting.apply _).tupled, Greeting.unapply)
}

Example: Lagom service read-side repository - Table query declaration

Following the declaration of a Slick Table, functional operations are callable through TableQuery class methods. The following example shows how a TableQuery class declarable for the HeartAI HelloWorldService:

def mapTable:
TableQuery[HelloWorldTable] =
  TableQuery[HelloWorldTable]

Example: Lagom service read-side repository - Table operations

The following examples show how functional table operations may be declared through TableQuery class methods:

generateDatabaseEntry()

def generateDatabaseEntry(
  greeting: Greeting):
DBIO[Done] = {
  greeting.id match {
    case queryGreeting =>
      findByIDQuery(queryGreeting)
        .flatMap {
          case None =>
            mapTable.insertOrUpdate(greeting)
          case _ =>
            DBIO.successful(Done)
        }
        .map(_ => Done)
        .transactionally
  }
}

removeDatabaseEntry()

def removeDatabaseEntry(
  id: String):
DBIOAction[Done.type, NoStream, Effect] = {
  val action = mapTable
    .filter(_.id === id)
    .delete
  database.run(action)
  DBIO.successful(Done)
}

findByID()

def findByID(
  id: String):
Future[Option[Greeting]] =
  database.run(findByIDQuery(id))

private def findByIDQuery(
  id: String):
DBIO[Option[Greeting]] =
  mapTable
    .filter(_.id === id)
    .result
    .headOption

These table operations correspond to the follow HelloWorldService service-level functionalities:

Table operation Functionality
generateDatabaseEntry() Inserts or updates Greeting at corresponding id index.
removeDatabaseEntry() Removes Greeting at corresponding id index.
findByID() Optionally finds Greeting at corresponding id index.

Example: Lagom service read-side repository - JDBC configuration

The following example shows a HeartAI HelloWorldService development environment Typesafe Config configuration of a Slick JDBC connection to PostgreSQL with HikariCP connection pooling:

# PostgreSQL
db.default {
  driver = "org.postgresql.Driver"
  url = "jdbc:postgresql://postgres:5432/heartai"
  username = heartai
  password = heartai
}
hikaricp {
  minimumIdle = 5
  maximumPoolSize = 10
}
jdbc-defaults.slick {
  profile = "slick.jdbc.PostgresProfile$"
}

Example: Lagom service read-side repository - Full declaration

The following example shows the full declaration of the read-side repository for the HeartAI HelloWorldService:

package net.heartai.hello_world

import java.time.Instant
import java.util.UUID

import akka.Done
import com.github.tminglei.slickpg._
import slick.dbio.DBIO
import slick.dbio.Effect
import slick.dbio.NoStream
import slick.lifted.ProvenShape
import slick.sql.FixedSqlAction
import slick.sql.FixedSqlStreamingAction

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.duration._

trait HAI_PostgresProfile
  extends ExPostgresProfile
    with PgArraySupport
    with PgDate2Support {

  def pgjson = "jsonb"

  override val api: HAI_API.type =
    HAI_API

  object HAI_API
    extends API
      with ArrayImplicits
      with DateTimeImplicits

}

import HAI_PostgresProfile.api._

object HAI_PostgresProfile
  extends HAI_PostgresProfile

class HelloWorldReadSideRepository(
  database: Database) {

  class HelloWorldTable(
    tag: Tag
  ) extends Table[Greeting](tag, "hello_world") {

    def id:
    Rep[String] =
      column[String]("id", O.PrimaryKey)

    def message:
    Rep[String] =
      column[String]("string")

    def * :
    ProvenShape[Greeting] =
      (id, message) <>
        ((Greeting.apply _).tupled, Greeting.unapply)
  }

  def mapTable:
  TableQuery[HelloWorldTable] =
    TableQuery[HelloWorldTable]

  def createTable():
  FixedSqlAction[Unit, NoStream, Effect.Schema] =
    mapTable.schema.createIfNotExists

  def generateDatabaseEntry(
    greeting: Greeting):
  DBIO[Done] = {
    greeting.id match {
      case queryGreeting =>
        findByIDQuery(queryGreeting)
          .flatMap {
            case None =>
              mapTable.insertOrUpdate(greeting)
            case _ =>
              DBIO.successful(Done)
          }
          .map(_ => Done)
          .transactionally
    }
  }

  def removeDatabaseEntry(
    id: String):
  DBIOAction[Done.type, NoStream, Effect] = {
    val action = mapTable
      .filter(_.id === id)
      .delete
    database.run(action)
    DBIO.successful(Done)
  }

  def findByID(
    id: String):
  Future[Option[Greeting]] =
    database.run(findByIDQuery(id))

  private def findByIDQuery(
    id: String):
  DBIO[Option[Greeting]] =
    mapTable
      .filter(_.id === id)
      .result
      .headOption
}

Lagom service loader

Example: Lagom service loader - Full declaration

package net.heartai.hello_world

import akka.cluster.sharding.typed.scaladsl.Entity
import com.lightbend.lagom.scaladsl.akka.discovery.AkkaDiscoveryComponents
import com.lightbend.lagom.scaladsl.api.Descriptor
import com.lightbend.lagom.scaladsl.api.LagomConfigComponent
import com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaComponents
import com.lightbend.lagom.scaladsl.cluster.ClusterComponents
import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents
import com.lightbend.lagom.scaladsl.persistence.slick.SlickPersistenceComponents
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
import com.lightbend.lagom.scaladsl.server._
import com.softwaremill.macwire._
import net.heartai.core.keycloak.KeycloakClientIntegrations
import org.pac4j.core.config.Config
import play.api.db.HikariCPComponents
import play.api.libs.ws.ahc.AhcWSComponents

class HelloWorldLoader
  extends LagomApplicationLoader {

  override def load(
    context: LagomApplicationContext):
  LagomApplication =
    new HelloWorldApplication(context)
      with AkkaDiscoveryComponents
      with HelloWorldComponents

  override def loadDevMode(
    context: LagomApplicationContext):
  LagomApplication =
    new HelloWorldApplication(context)
      with LagomDevModeComponents
      with HelloWorldComponents

  override def describeService:
  Option[Descriptor] =
    Some(readDescriptor[HelloWorldServiceAPI])
}

abstract class HelloWorldApplication(
  context: LagomApplicationContext)
  extends LagomApplication(context)
    with LagomServerComponents
    with LagomConfigComponent
    with SlickPersistenceComponents
    with HikariCPComponents
    with ClusterComponents
    with AhcWSComponents {

  override lazy val lagomServer: LagomServer =
    serverFor[HelloWorldServiceAPI](wire[HelloWorldServiceIMPL])

  override lazy val jsonSerializerRegistry:
    JsonSerializerRegistry =
    HelloWorldSerialiserRegistry


  lazy val reportRepository:
    HelloWorldReadSideRepository =
    wire[HelloWorldReadSideRepository]
  readSide.register(wire[HelloWorldReadSideProcessor])

  lazy val eventProcessor:
    HelloWorldReadSideProcessor =
    wire[HelloWorldReadSideProcessor]

  clusterSharding.init(
    Entity(HelloWorldState.typeKey) {
      entityContext =>
        HelloWorldEntity.create(entityContext)
    })

  lazy val serviceConfig: Config =
    KeycloakClientIntegrations.serviceConfig
}

trait HelloWorldComponents
  extends LagomKafkaComponents

Lagom service testing

Example: Lagom service testing - Full declaration

package net.heartai.hello_world

import akka.Done
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.pattern.StatusReply
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit
import akka.persistence.typed.PersistenceId
import com.lightbend.lagom.scaladsl.server.LocalServiceLocator
import com.lightbend.lagom.scaladsl.testkit.ReadSideTestDriver
import com.lightbend.lagom.scaladsl.testkit.ServiceTest
import com.lightbend.lagom.scaladsl.testkit.TestTopicComponents
import com.typesafe.config.ConfigFactory
import net.heartai.core.PingMsg
import org.scalatest.BeforeAndAfterEach
import org.scalatest.wordspec.AsyncWordSpecLike

import scala.concurrent.ExecutionContext

class HelloWorldServiceSpec
  extends ScalaTestWithActorTestKit(
    ConfigFactory.parseString(
      "akka.actor.allow-java-serialization = on")
      .withFallback(EventSourcedBehaviorTestKit.config))
    with AsyncWordSpecLike
    with BeforeAndAfterEach {

  System.setProperty("heartai.services.testing", "true")

  implicit private val ec: ExecutionContext =
    scala.concurrent.ExecutionContext.Implicits.global

  private val server = ServiceTest.startServer(
    ServiceTest.defaultSetup.withCluster()
  ) { ctx =>
    new HelloWorldApplication(ctx)
      with LocalServiceLocator
      with TestTopicComponents {

      override lazy val readSide: ReadSideTestDriver =
        new ReadSideTestDriver()(materializer, executionContext)
    }
  }

  protected override def afterAll(): Unit =
    server.stop()

  private val client: HelloWorldServiceAPI =
    server.serviceClient.implement[HelloWorldServiceAPI]

  "HelloWorldService" should {
    "implement service endpoint behaviour for pingService" in {
      client.pingService()
        .invoke()
        .map(_.msg.isDefined shouldBe true)
    }
  }

  private val pingMsg: PingMsg =
    PingMsg(
      msg = Some("Hello"))

  "HelloWorldService" should {
    "implement service endpoint behaviour for pingServiceByPOST" in {
      client.pingServiceByPOST()
        .invoke(pingMsg)
        .map(_.msg shouldBe pingMsg.msg)
    }
  }

  private val testGreeting: GreetingIMPL =
    GreetingIMPL(
      msg = "Bonjour")

  private val testEntity =
    EventSourcedBehaviorTestKit[HelloWorldCommand, HelloWorldEvent, HelloWorldState](
      system,
      HelloWorldEntity.create(
        PersistenceId("HelloEntity", "1")))

  "HelloWorldService" should {
    "implement event-sourced behaviour for useGreeting" in {
      val result = testEntity.runCommand[StatusReply[Done]](
        UpdateGreetingMessageCommand(testGreeting.msg, _))
      result.reply shouldBe StatusReply.Success(Done)
      result.event shouldBe GreetingMessageUpdatedEvent(testGreeting.msg)
      result.state.msg shouldBe testGreeting.msg
    }
  }
}

Lagom service deployment

Services developed with the Lagom microservices framework may be packaged and deployed to Kubernetes-compliant platforms such as the Red Hat OpenShift container orchestration platform. This provides a cloud-native approach to service deployment, allowing for highly distributed, concurrent, and available systems.

OpenShift implementation

HeartAI orchestrates system services with the Kubernetes-based Red Hat OpenShift container platform. Further information about the HeartAI implementation of Red Hat OpenShift may be found with the following documentation:

The following examples from the HeartAI production environment HelloWorldService show how Lagom developed services may be deployed to Red Hat OpenShift. These declaration files are specified with the YAML serialisation language and correspond to OpenShift / Kubernetes resources.

Example: Lagom service deployment - OpenShift Deployment

OpenShift Deployments allow the declaration of Pod deployments and higher-level configuration for how these Pods should be orchestrated and managed within the OpenShift environment. The OpenShift Deployment Controller synchronises the actual state of the cluster to the declared state with configurable controls for how this synchronisation operates.

The following example shows a Deployment declaration from the HeartAI HelloWorldService production environment:

apiVersion: "apps/v1"
kind: Deployment
metadata:
  name: heartai-service-hello-world-prod
  namespace: heartai-hello-world-prod
  labels:
    app: heartai-service-hello-world-prod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: heartai-service-hello-world-prod
  strategy:
    rollingUpdate:
      maxSurge: 3
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: heartai-service-hello-world-prod
        version: v0.33.62
        actorSystemName: heartai-service-hello-world-prod
      annotations:
        sidecar.istio.io/inject: "true"
        traffic.sidecar.istio.io/includeInboundPorts: "2552,8558,14000,14020"
        traffic.sidecar.istio.io/excludeOutboundPorts: "2552,8558"
    spec:
      containers:
        - name: heartai-service-hello-world-prod
          image: "quay.io/heartai/heartai-hello-world:0.33.62"
          imagePullPolicy: Always
          livenessProbe:
            httpGet:
              path: /alive
              port: management
            initialDelaySeconds: 20
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: management
            initialDelaySeconds: 20
            periodSeconds: 10
          ports:
            - name: remoting
              containerPort: 2552
              protocol: TCP
            - name: management
              containerPort: 8558
              protocol: TCP
            - name: http
              containerPort: 14000
              protocol: TCP
            - name: https
              containerPort: 14020
              protocol: TCP
          env:
            - name: OS_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: JAVA_OPTS
              value: "-Xms1024m -Xmx1024m -Dconfig.resource=production.conf"
            - name: APPLICATION_SECRET
              valueFrom:
                secretKeyRef:
                  name: heartai-play-secret
                  key: secret
            - name: AKKA_DISCOVERY_SERVICE_NAME
              value: "heartai-service-hello-world-prod"
            - name: AKKA_CLUSTER_BOOTSTRAP_SERVICE_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: "metadata.labels['app']"
            - name: REQUIRED_CONTACT_POINT_NR
              value: "1"
            #            - name: AKKA_REMOTING_HOST
            #              value: "heartai-service-hello-world-prod.heartai-hello-world-prod.svc.cluster.local"
            - name: AKKA_REMOTING_PORT
              value: "2552"
            - name: AKKA_MANAGEMENT_PORT
              value: "8558"
            - name: HTTP_BIND_ADDRESS
              value: "0.0.0.0"
            - name: HTTP_PORT
              value: "14000"
            - name: HTTPS_PORT
              value: "14020"
            - name: KAFKA_SERVICE_NAME
              value: "strimzi-kafka-kafka-brokers.heartai-strimzi.svc.cluster.local:9092"
            - name: KAFKA_BROKERS_SERVICE_URL
              value: "strimzi-kafka-kafka-bootstrap.heartai-strimzi.svc.cluster.local:9092"
            - name: POSTGRESQL_CONTACT_POINT
              valueFrom:
                secretKeyRef:
                  name: postgres-url
                  key: secret
            - name: POSTGRESQL_USERNAME
              valueFrom:
                secretKeyRef:
                  name: postgres-id
                  key: secret
            - name: POSTGRESQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-key
                  key: secret
          resources:
            limits:
              cpu: 1000m
              memory: 2048Mi
            requests:
              cpu: 200m
              memory: 1048Mi

Example: Lagom service deployment - OpenShift Service

OpenShift Services provide a cluster-internal access point to corresponding network address spaces. For network routing to OpenShift Pods, Services allow consistent resolution to the virtual IP address space of one-or-more Pods, noting that such Pods may generally be transient. Access to deployment Pods through a Service is location transparent, scalable, and tolerant to Pod failure. Services may also be configured with load balancing, port-forwarding capability, and session affinity.

Service discovery

HeartAI services provide approaches for location transparent service discovery within corresponding HeartAI Red Hat OpenShift container platform instances. Further information about HeartAI service discovery may be found with the following documentation:

The following example shows a Service declaration from the HeartAI HelloWorldService production environment:

apiVersion: v1
kind: Service
metadata:
  name: heartai-service-hello-world-prod
  namespace: heartai-hello-world-prod
spec:
  ports:
    - name: http
      port: 80
      targetPort: 14000
  selector:
    app: heartai-service-hello-world-prod
  type: LoadBalancer

Example: Lagom service deployment - OpenShift Role

OpenShift Roles provide specifications to configure access control within the OpenShift environment.

The following example shows a Role declaration from the HeartAI HelloWorldService production environment. This particular Role declaration provides the permission required for the HelloWorldService service Deployment to be able to locate corresponding Pods of the service, fulfilling the ability to provide location transparent service discovery.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: heartai-pod-reader
  namespace: heartai-hello-world-prod
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "watch", "list"]

Example: Lagom service deployment- OpenShift Role Binding

The following example shows a Role Binding declaration from the HeartAI HelloWorldService production environment.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: heartai-hello-world-prod
subjects:
  - kind: ServiceAccount
    name: default
roleRef:
  kind: Role
  name: heartai-pod-reader
  apiGroup: rbac.authorization.k8s.io

Example: Lagom service deployment - Istio Gateway

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: heartai-hello-gw
  namespace: heartai-hello-world-prod
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      tls:
        httpsRedirect: true
      hosts:
        - hello.prod.apps.aro.sah.heartai.net
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: heartai-hello-world-prod-gw-cert
      hosts:
        - hello.prod.apps.aro.sah.heartai.net

Example: Lagom service deployment - Istio Virtual Service

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: heartai-service-hello-world-vs-prod
  namespace: heartai-hello-world-prod
spec:
  hosts:
    - heartai-service-hello-world-prod.heartai-hello-world-prod.svc.cluster.local
  gateways:
    - mesh
  #      - heartai-service-hello-world-gw
  http:
    - match:
        - uri:
            prefix: "/"
      route:
        - destination:
            host: heartai-service-hello-world-prod

Example: Lagom service deployment - Istio Destination Rule

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: heartai-service-hello-world-rule-prod
  namespace: heartai-hello-world-prod
spec:
  host: heartai-service-hello-world-prod
  subsets:
    - name: stable
      labels:
        version: v0.33.62

Example: Lagom service deployment - Istio Service Entry

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: postgresql
  namespace: heartai-hello-world-prod
spec:
  hosts:
    - sah-heartai-psql-prod-aue-001.postgres.database.azure.com
  ports:
    - number: 5432
      name: postgresql
      protocol: tcp
  resolution: DNS
  location: MESH_EXTERNAL
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: kafka-brokers
  namespace: heartai-hello-world-prod
spec:
  hosts:
    - strimzi-kafka-kafka-brokers.heartai-strimzi.svc.cluster.local
  ports:
    - number: 9092
      name: kafka-brokers
      protocol: tcp
  resolution: DNS
  location: MESH_EXTERNAL
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: kafka-bootstrap
  namespace: heartai-hello-world-prod
spec:
  hosts:
    - strimzi-kafka-kafka-bootstrap.heartai-strimzi.svc.cluster.local
  ports:
    - number: 9092
      name: kafka-bootstrap
      protocol: tcp
  resolution: DNS
  location: MESH_EXTERNAL