A Blueprint for Scala Microservices
- 9. BUSINESS LOGIC = AKKA ACTORS
object SumService {
def props = Props(classOf[SumService])
!
case class SumOp(a: Int, b: Int)
case class SumRes(result: Int)
}
!
class SumService extends Actor {
import SumService._
!
def receive: Receive = {
case SumOp(a, b) => sender() ! SumRes(a + b)
}
}
- 10. REST API = SPRAY.IO
Sum
Service
SPRAY
API
SumOp
SumRes
GET /v1/sum
JSON
- 11. REST API = SPRAY.IO
case class SumApiResponse(result: Int)
!
object SumProtocol extends DefaultJsonProtocol {
implicit val sumApiResponseFormat = jsonFormat1(SumApiResponse)
}
!
class SumHttpServiceV1(services: Services) {
import SumProtocol._
!
def route = path("v1" / "sum") {
get {
parameters('a.as[Int], 'b.as[Int]) { (a, b) =>
def future = (services.sumService ? SumOp(a, b)).mapTo[SumRes]
onSuccess(future) { response =>
complete(SumApiResponse(response.result))
}
}
}
}
}
- 12. API DOCS = SWAGGER
SPRAY-
SWAGGER
GET /
HTML/JSON
docs
- 13. API DOCS = SWAGGER
@Api(value = “/v1/sum", description = "Sum numbers.")
class SumHttpServiceV1(services: Services) {
…
}
- 14. API DOCS = SWAGGER
!
@ApiOperation(value = "Returns the sum”, httpMethod = “GET")
!
@ApiImplicitParams(Array(
new ApiImplicitParam(
name="a", required=true, dataType="integer", paramType=“query"
),
new ApiImplicitParam(
name="b", required=true, dataType="integer", paramType=“query"
)
))
!
@ApiResponses(Array(
new ApiResponse(
code=200, message="The sum", response=classOf[SumApiResponse]
)
))
!
def route = path("v1" / "sum") { … }
- 15. API DOCS = SWAGGER
@ApiModel(description = "Result of the sum")
case class SumApiResponse(
@(ApiModelProperty @field)(value = "The sum of a and b")
result: Int
)
- 17. CONFIG = TYPESAFE CONFIG
application.conf
development.conf
testing.conf staging.conf
production.conf
- 18. CONFIG = TYPESAFE CONFIG
akka.loglevel = "INFO"
!
metrics.carbon = "graphite.local:2003"
!
acme {
environment = "production"
!
svc-calculator {
hostname = “0.0.0.0”
port = "9999"
}
}
application.conf
- 20. CONFIG = TYPESAFE CONFIG
include "application"
!
akka.loglevel = "DEBUG"
metrics.carbon = "localhost:2003"
!
acme {
environment = "development"
!
svc-calculator {
hostname = "127.0.0.1"
}
}
development.conf
- 21. CONFIG = TYPESAFE CONFIG
val config = ConfigFactory.defaultApplication()
$ sbt -Dconfig.resource=development.conf run
Loads application.conf by default
Loads development.conf
- 22. CONFIG = TYPESAFE CONFIG
{
mysql_username=${MYSQL_USERNAME}
mysql_password=${MYSQL_PASSWORD}
}
PRO-TIP
Don’t store passwords with source code!
Instead make use ENV vars substitution and
keep passwords in a safe place.
- 23. HEALTH = SPRAY + HAPROXY
SPRAY
GET /ping
Load
Balancer
Are you ok?
- 24. HEALTH = SPRAY + HAPROXY
def route = get {
path("ping") {
complete("pong")
}
}
- 26. PERF.MON. = METRICS + GRAPHITE
object MetricsInstrumentation {
val metricRegistry = new MetricRegistry()
}
!
trait Instrumented extends InstrumentedBuilder {
val metricRegistry = MetricsInstrumentation.metricRegistry
}
!
class Actor extends Actor with Instrumented {
val meter = metrics.meter("requests")
!
def receive: Receive = {
!
case Request =>
meter.mark()
!
}
}
- 29. PACKAGING = ASSEMBLY + DOCKER
JVM
SERVICE
(fat jar)
HTTP Requests
METRICS
“Immutable” container
Other Svc HTTP Reqs
- 30. PACKAGING = SBT-ASSEMBLY + SBT-DOCKER
docker <<= (docker dependsOn assembly)
!
dockerfile in docker := {
val artifact = (outputPath in assembly).value
val artifactTargetPath = s"/app/${artifact.name}"
new Dockerfile {
from(“acme/javabase")
expose(9999)
add(artifact, artifactTargetPath)
entryPoint("java", "-jar", artifactTargetPath)
}
}
!
imageNames in docker := Seq(
ImageName(
namespace = Some(organization.value),
repository = name.value,
tag = Some("v" + version.value)
)
)
- 31. MINIMUM VIABLE SERVICE
Business Logic = Akka
REST API = Spray + Swagger
Configuration = Typesafe Config
Health / Monitoring = Metrics
Packaging = Assembly & Docker
- 33. CI/DEPLOY = JENKINS + SBT + ANSIBLE
Git repo
Jenkins Jobs + sbt
Test Release Dockerize Deploy
Commit
Staging / Production
Ansible
- 35. {
"id": "/svc-calculator",
"container": {
"type": "DOCKER",
"docker": {
"image": "docker.com/svc-calculator:1.0.0",
"network": "BRIDGE",
"portMappings": [{
"containerPort": 9999, "protocol": "tcp"
}]
}
},
"env": {"JAVA_ARGS": “-Dconfig.resource=production"},
"cpus": 1,
"mem": 1024,
"instances": 2
}
HOSTING = MESOS/MARATHON
- 38. DISCOVERY = BAMBOO + HAPROXY
mesos1
/svc-calculator /svc-calculator
mesos2
/web-app
mesos3
BAMBOO +
HAPROXY
BAMBOO +
HAPROXY
BAMBOO +
HAPROXY
MARATHON
http://localhost/svc-calculator
:80
:30001:30002
- 41. DRY: CUSTOM SBT PLUGIN
package com.acme.sbt.plugin
import sbt._
import Keys._
object AcmePlugin extends AutoPlugin {
lazy val acmeBuildSettings = Seq(
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-feature",
"-Xlint",
"-Ywarn-dead-code",
"-target:jvm-1.7"
)
)
} AcmePlugin.scala
- 42. FINAL TIP: CUSTOM SBT PLUGIN
import com.acme.sbt.plugin.AcmePlugin._
!
Seq(acmeBuildSettings:_*)
!
// custom project settings
build.sbt
- 43. ZERO FRICTION: GITER8 TEMPLATES
% g8 file://g8-svc-spray/ --name=svc-calculator
-—servicePort=9999
!
Template applied in ./svc-calculator
!
% ls svc-calculator
build.sbt project src version.sbt