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:
- Services-based architectures that favour reactive microservices design principles.
- Native integrations of the Akka actor-based concurrency library and the Play web services framework.
- State and persistence behaviour that allows event sourcing and CQRS models.
- Lightweight and high-performance abstractions for application-programming interfaces.
- Modern service capabilities such as distributability with Akka Cluster and circuit breaker constructs.
Although Lagom is relatively opinionated as a framework, the native implementation of Akka and Play allows for diverse flexibility and extensibility.
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 serialisation
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 {
artery {
transport = tcp
}
}
# PostgreSQL
db.default {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://localhost: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
}
}
}
# 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.conf"
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:
remote {
artery {
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.conf"
# HeartAI
heartai.local_mode = false
heartai.service_topic.greeting_messages_changed = ${SERVICE_TOPIC_GREETING_MESSAGES_CHANGED}
# Akka
akka.discovery {
method = akka-dns
}
coordinated-shutdown.exit-jvm = on
cluster {
shutdown-after-unsuccessful-join-seed-nodes = 60s
}
# Akka Remoting
remote {
artery {
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 = "heartai-hello-world"
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.
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:
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/public/ping", pingService()),
restCall(Method.POST, "/hello/api/public/ping", pingServiceByPOST()),
restCall(Method.GET, "/hello/api/public/ping_ws_count", pingServiceByWebSocketCount),
restCall(Method.GET, "/hello/api/public/ping_ws_echo", pingServiceByWebSocketEcho),
restCall(Method.GET, "/hello/api/public/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/public/ping", pingService()),
restCall(Method.POST, "/hello/api/public/ping", pingServiceByPOST()),
restCall(Method.GET, "/hello/api/public/ping_ws_count", pingServiceByWebSocketCount),
restCall(Method.GET, "/hello/api/public/ping_ws_echo", pingServiceByWebSocketEcho),
restCall(Method.GET, "/hello/api/public/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.
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.
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.
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 Command
s 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 Command
s 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 GreetingCommand s will respond with the updated greeting message. |
Example: Lagom service entity - Entity events
The following example shows the service entity Event
s 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 Events
s 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
:
The following example shows the UpdateGreetingMessageCommand
event-sourcing process for the HeartAI HelloWorldService
:
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
:
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://localhost: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.KeycloakCertsHTTPRequest
import org.pac4j.core.config.Config
import org.pac4j.core.context.HttpConstants.AUTHORIZATION_HEADER
import org.pac4j.core.context.HttpConstants.BEARER_HEADER_PREFIX
import org.pac4j.core.context.WebContext
import org.pac4j.core.profile.CommonProfile
import org.pac4j.http.client.direct.HeaderClient
import org.pac4j.lagom.jwt.JwtAuthenticatorHelper
import play.api.db.HikariCPComponents
import play.api.libs.ws.ahc.AhcWSComponents
import java.util
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
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 jwtClient: HeaderClient = {
val headerClient =
KeycloakCertsHTTPRequest.keycloakConfig
.map {
pac4jConfig =>
val buildHeaderClient = new HeaderClient
buildHeaderClient.setName("jwt_header")
buildHeaderClient.setHeaderName(AUTHORIZATION_HEADER)
buildHeaderClient.setPrefixHeader(BEARER_HEADER_PREFIX)
buildHeaderClient.setAuthenticator(JwtAuthenticatorHelper.parse(pac4jConfig.getConfig("pac4j.lagom.jwt.authenticator")))
buildHeaderClient.setAuthorizationGenerator((_: WebContext, profile: CommonProfile) => {
if (profile.containsAttribute("groups"))
profile.addRoles(profile.getAttribute("groups", classOf[util.Collection[String]]))
profile
})
buildHeaderClient
}
Await.result(headerClient, 3.seconds)
}
lazy val serviceConfig: Config = {
val config = new Config(jwtClient)
config.getClients.setDefaultSecurityClients(jwtClient.getName)
config
}
}
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.
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-hello-world
namespace: heartai-hello-world-prod
labels:
app: heartai-hello-world
spec:
replicas: 1
selector:
matchLabels:
app: heartai-hello-world
strategy:
rollingUpdate:
maxSurge: 3
maxUnavailable: 1
template:
metadata:
labels:
app: heartai-hello-world
version: v0.31.106
actorSystemName: heartai-hello-world
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-hello-world
image: "quay.io/heartai/heartai-hello-world:0.31.106"
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_CLUSTER_BOOTSTRAP_SERVICE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: "metadata.labels['app']"
- name: REQUIRED_CONTACT_POINT_NR
value: "1"
- 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_BROKERS_SERVICE
value: "strimzi-kafka-kafka-brokers.heartai-strimzi.svc.cluster.local:9092"
- name: KAFKA_BOOTSTRAP_SERVICE
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
- name: SERVICE_TOPIC_GREETING_MESSAGES_CHANGED
value: "hello_world_greeting_messages_changed_prod"
resources:
limits:
cpu: 500m
memory: 2048Mi
requests:
cpu: 100m
memory: 1024Mi
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.
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-hello-world
namespace: heartai-hello-world-prod
spec:
ports:
- name: http
port: 80
targetPort: 14000
selector:
app: heartai-hello-world
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-world-prod-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-hello-world-vs
namespace: heartai-hello-world-prod
spec:
hosts:
- hello.prod.apps.aro.sah.heartai.net
gateways:
- heartai-hello-world-prod-gw
http:
- match:
- uri:
prefix: "/"
route:
- destination:
host: heartai-hello-world
Example: Lagom service deployment - Istio Destination Rule
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: heartai-hello-world-nw-rule
namespace: heartai-hello-world-prod
spec:
host: heartai-hello-world
subsets:
- name: stable
labels:
version: v0.31.106
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