Production Deployment and DevOps: Enterprise Scala Application Lifecycle
Successfully deploying and managing Scala applications in production requires comprehensive understanding of modern DevOps practices, containerization, monitoring, and infrastructure automation. This lesson covers enterprise-grade deployment strategies, monitoring solutions, and operational best practices.
Containerization and Application Packaging
Docker Integration for Scala Applications
// Build configuration for containerized applications
// build.sbt
ThisBuild / version := "1.0.0"
ThisBuild / scalaVersion := "2.13.12"
lazy val root = (project in file("."))
.enablePlugins(JavaAppPackaging, DockerPlugin)
.settings(
name := "scala-production-app",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.5.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0",
"com.typesafe.akka" %% "akka-actor-typed" % "2.8.0",
"ch.qos.logback" % "logback-classic" % "1.4.11",
"net.logstash.logback" % "logstash-logback-encoder" % "7.4",
"io.prometheus" % "simpleclient" % "0.16.0",
"io.prometheus" % "simpleclient_hotspot" % "0.16.0",
"io.prometheus" % "simpleclient_httpserver" % "0.16.0",
"com.typesafe" % "config" % "1.4.2"
),
// Docker configuration
Docker / packageName := "scala-app",
Docker / version := version.value,
Docker / daemonUser := "appuser",
dockerBaseImage := "openjdk:17-jre-slim",
dockerExposedPorts := Seq(8080, 9090), // App and metrics ports
dockerRepository := Some("your-registry.com"),
// Multi-stage Docker build
dockerCommands := {
val appDir = "/opt/app"
val configDir = s"$appDir/conf"
val logsDir = s"$appDir/logs"
Seq(
Cmd("FROM", "openjdk:17-jre-slim"),
Cmd("LABEL", "maintainer=\"Your Team <team@company.com>\""),
// Install system dependencies
Cmd("RUN", "apt-get update && apt-get install -y curl dumb-init && rm -rf /var/lib/apt/lists/*"),
// Create app user and directories
Cmd("RUN", s"useradd -m -u 1001 appuser"),
Cmd("RUN", s"mkdir -p $appDir $configDir $logsDir"),
Cmd("RUN", s"chown -R appuser:appuser $appDir"),
// Copy application
Cmd("COPY", "--chown=appuser:appuser opt/docker/lib /opt/app/lib"),
Cmd("COPY", "--chown=appuser:appuser opt/docker/bin /opt/app/bin"),
Cmd("COPY", "--chown=appuser:appuser opt/docker/conf /opt/app/conf"),
// Health check
Cmd("HEALTHCHECK", "--interval=30s --timeout=3s --start-period=5s --retries=3",
"CMD curl -f http://localhost:8080/health || exit 1"),
Cmd("USER", "appuser"),
Cmd("WORKDIR", appDir),
Cmd("ENTRYPOINT", "[\"dumb-init\", \"--\"]"),
Cmd("CMD", "[\"bin/scala-production-app\"]")
)
}
)
// Multi-stage Dockerfile template
val dockerfileTemplate =
"""
# Multi-stage build for Scala applications
FROM sbt:1.9.6-openjdk-17 as builder
WORKDIR /app
COPY build.sbt .
COPY project/ project/
RUN sbt update
COPY src/ src/
RUN sbt stage
FROM openjdk:17-jre-slim as runtime
# Install runtime dependencies
RUN apt-get update && \
apt-get install -y curl dumb-init netcat-openbsd && \
rm -rf /var/lib/apt/lists/*
# Create application user
RUN useradd -m -u 1001 appuser
# Set up application directory
WORKDIR /opt/app
COPY --from=builder --chown=appuser:appuser /app/target/universal/stage/ .
# Create required directories
RUN mkdir -p logs conf data && \
chown -R appuser:appuser /opt/app
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
USER appuser
EXPOSE 8080 9090
ENTRYPOINT ["dumb-init", "--"]
CMD ["bin/scala-production-app"]
"""
Production-Ready Application Structure
// Production application configuration
import com.typesafe.config.{Config, ConfigFactory}
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Route
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
import java.util.concurrent.TimeUnit
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.hotspot.DefaultExports
// Main application entry point
object ProductionApp {
def main(args: Array[String]): Unit = {
// Load configuration with environment-specific overrides
val config = loadConfiguration()
// Initialize metrics collection
initializeMetrics()
// Start the application
implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "production-app", config)
implicit val ec: ExecutionContext = system.executionContext
val app = new ProductionApp(config)
app.start().onComplete {
case Success(binding) =>
val localAddress = binding.localAddress
system.log.info(s"Server online at http://${localAddress.getHostString}:${localAddress.getPort}/")
// Register shutdown hook
sys.addShutdownHook {
system.log.info("Shutting down application...")
app.shutdown().onComplete(_ => system.terminate())
}
case Failure(exception) =>
system.log.error("Failed to start server", exception)
system.terminate()
}
}
private def loadConfiguration(): Config = {
val environment = sys.env.getOrElse("ENVIRONMENT", "development")
// Load configuration with environment-specific overrides
val baseConfig = ConfigFactory.load()
val envConfig = ConfigFactory.load(s"application-$environment.conf")
envConfig.withFallback(baseConfig).resolve()
}
private def initializeMetrics(): Unit = {
// Register JVM metrics
DefaultExports.initialize()
// Initialize custom metrics
ApplicationMetrics.initialize()
}
}
class ProductionApp(config: Config)(implicit system: ActorSystem[Nothing], ec: ExecutionContext) {
import akka.http.scaladsl.server.Directives._
private val httpConfig = config.getConfig("app.http")
private val host = httpConfig.getString("host")
private val port = httpConfig.getInt("port")
private val metricsPort = httpConfig.getInt("metrics-port")
// Application components
private val healthCheck = new HealthCheckService(config)
private val businessLogic = new BusinessLogicService(config)
private val metricsService = new MetricsService()
// Application routes
private val routes: Route = {
pathPrefix("api" / "v1") {
businessLogic.routes
} ~
path("health") {
healthCheck.route
} ~
path("ready") {
healthCheck.readinessRoute
} ~
path("metrics") {
metricsService.route
}
}
def start(): Future[Http.ServerBinding] = {
// Start metrics server
startMetricsServer()
// Start main application server
Http().newServerAt(host, port).bind(routes)
}
private def startMetricsServer(): Future[Http.ServerBinding] = {
val metricsRoute = path("metrics") {
metricsService.route
}
Http().newServerAt(host, metricsPort).bind(metricsRoute)
}
def shutdown(): Future[Unit] = {
for {
_ <- businessLogic.shutdown()
_ <- healthCheck.shutdown()
} yield ()
}
}
// Health check implementation
class HealthCheckService(config: Config)(implicit system: ActorSystem[Nothing], ec: ExecutionContext) {
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
import spray.json._
import DefaultJsonProtocol._
case class HealthStatus(
status: String,
timestamp: Long,
uptime: Long,
version: String,
checks: Map[String, CheckResult]
)
case class CheckResult(status: String, message: String, responseTime: Long)
implicit val checkResultFormat: RootJsonFormat[CheckResult] = jsonFormat3(CheckResult)
implicit val healthStatusFormat: RootJsonFormat[HealthStatus] = jsonFormat5(HealthStatus)
private val startTime = System.currentTimeMillis()
private val version = config.getString("app.version")
private val databaseChecker = new DatabaseHealthChecker(config)
private val externalServiceChecker = new ExternalServiceHealthChecker(config)
val route: Route = {
get {
complete {
performHealthCheck().map { status =>
val httpStatus = if (status.status == "healthy") StatusCodes.OK else StatusCodes.ServiceUnavailable
val entity = HttpEntity(ContentTypes.`application/json`, status.toJson.prettyPrint)
(httpStatus, entity)
}
}
}
}
val readinessRoute: Route = {
get {
complete {
performReadinessCheck().map { isReady =>
if (isReady) StatusCodes.OK else StatusCodes.ServiceUnavailable
}
}
}
}
private def performHealthCheck(): Future[HealthStatus] = {
val checksF = Future.sequence(Seq(
checkDatabase(),
checkExternalServices(),
checkMemoryUsage(),
checkDiskSpace()
))
checksF.map { checks =>
val checkMap = checks.toMap
val overallStatus = if (checks.forall(_._2.status == "healthy")) "healthy" else "unhealthy"
HealthStatus(
status = overallStatus,
timestamp = System.currentTimeMillis(),
uptime = System.currentTimeMillis() - startTime,
version = version,
checks = checkMap
)
}
}
private def performReadinessCheck(): Future[Boolean] = {
// Readiness check is simpler - just check if essential services are available
for {
dbReady <- databaseChecker.isReady()
servicesReady <- externalServiceChecker.areEssentialServicesReady()
} yield dbReady && servicesReady
}
private def checkDatabase(): Future[(String, CheckResult)] = {
val startTime = System.nanoTime()
databaseChecker.check().map { isHealthy =>
val responseTime = (System.nanoTime() - startTime) / 1000000 // Convert to milliseconds
val result = CheckResult(
status = if (isHealthy) "healthy" else "unhealthy",
message = if (isHealthy) "Database connection OK" else "Database connection failed",
responseTime = responseTime
)
("database", result)
}.recover {
case ex =>
val responseTime = (System.nanoTime() - startTime) / 1000000
("database", CheckResult("unhealthy", s"Database check failed: ${ex.getMessage}", responseTime))
}
}
private def checkExternalServices(): Future[(String, CheckResult)] = {
val startTime = System.nanoTime()
externalServiceChecker.checkAll().map { results =>
val responseTime = (System.nanoTime() - startTime) / 1000000
val allHealthy = results.forall(_._2)
val message = if (allHealthy) "All external services OK" else s"Some services failing: ${results.filter(!_._2).map(_._1).mkString(", ")}"
val result = CheckResult(
status = if (allHealthy) "healthy" else "degraded",
message = message,
responseTime = responseTime
)
("external-services", result)
}
}
private def checkMemoryUsage(): Future[(String, CheckResult)] = {
Future {
val runtime = Runtime.getRuntime
val maxMemory = runtime.maxMemory()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val memoryUsagePercent = (usedMemory.toDouble / maxMemory.toDouble) * 100
val status = if (memoryUsagePercent < 80) "healthy" else if (memoryUsagePercent < 90) "warning" else "critical"
val message = f"Memory usage: ${memoryUsagePercent}%.1f%% (${usedMemory / 1024 / 1024}MB / ${maxMemory / 1024 / 1024}MB)"
("memory", CheckResult(status, message, 0))
}
}
private def checkDiskSpace(): Future[(String, CheckResult)] = {
Future {
val file = new java.io.File(".")
val totalSpace = file.getTotalSpace
val freeSpace = file.getFreeSpace
val usedSpace = totalSpace - freeSpace
val diskUsagePercent = (usedSpace.toDouble / totalSpace.toDouble) * 100
val status = if (diskUsagePercent < 80) "healthy" else if (diskUsagePercent < 90) "warning" else "critical"
val message = f"Disk usage: ${diskUsagePercent}%.1f%% (${usedSpace / 1024 / 1024 / 1024}GB / ${totalSpace / 1024 / 1024 / 1024}GB)"
("disk", CheckResult(status, message, 0))
}
}
def shutdown(): Future[Unit] = {
for {
_ <- databaseChecker.shutdown()
_ <- externalServiceChecker.shutdown()
} yield ()
}
}
// Database health checker
class DatabaseHealthChecker(config: Config)(implicit ec: ExecutionContext) {
// Implementation would depend on your database setup
def check(): Future[Boolean] = {
Future {
// Perform actual database health check
// Example: execute a simple query
true
}
}
def isReady(): Future[Boolean] = check()
def shutdown(): Future[Unit] = Future.successful(())
}
// External service health checker
class ExternalServiceHealthChecker(config: Config)(implicit system: ActorSystem[Nothing], ec: ExecutionContext) {
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, StatusCodes}
import scala.concurrent.duration._
private val externalServices = config.getConfigList("app.external-services").asScala.map { serviceConfig =>
serviceConfig.getString("name") -> serviceConfig.getString("health-url")
}.toMap
def checkAll(): Future[Map[String, Boolean]] = {
val checks = externalServices.map { case (name, url) =>
checkService(url).map(name -> _)
}
Future.sequence(checks).map(_.toMap)
}
def areEssentialServicesReady(): Future[Boolean] = {
checkAll().map(_.values.forall(identity))
}
private def checkService(url: String): Future[Boolean] = {
Http().singleRequest(HttpRequest(uri = url))
.map(_.status == StatusCodes.OK)
.recover(_ => false)
}
def shutdown(): Future[Unit] = Future.successful(())
}
Monitoring and Observability
Comprehensive Metrics Collection
// Prometheus metrics integration
import io.prometheus.client._
import io.prometheus.client.hotspot.DefaultExports
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
object ApplicationMetrics {
// Business metrics
val requestsTotal: Counter = Counter.build()
.name("http_requests_total")
.help("Total number of HTTP requests")
.labelNames("method", "endpoint", "status")
.register()
val requestDuration: Histogram = Histogram.build()
.name("http_request_duration_seconds")
.help("HTTP request duration in seconds")
.labelNames("method", "endpoint")
.buckets(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
.register()
val activeConnections: Gauge = Gauge.build()
.name("http_active_connections")
.help("Number of active HTTP connections")
.register()
val businessOperationsTotal: Counter = Counter.build()
.name("business_operations_total")
.help("Total number of business operations")
.labelNames("operation", "result")
.register()
val businessOperationDuration: Histogram = Histogram.build()
.name("business_operation_duration_seconds")
.help("Business operation duration in seconds")
.labelNames("operation")
.register()
// Database metrics
val databaseConnectionsActive: Gauge = Gauge.build()
.name("database_connections_active")
.help("Number of active database connections")
.register()
val databaseQueriesTotal: Counter = Counter.build()
.name("database_queries_total")
.help("Total number of database queries")
.labelNames("query_type", "result")
.register()
val databaseQueryDuration: Histogram = Histogram.build()
.name("database_query_duration_seconds")
.help("Database query duration in seconds")
.labelNames("query_type")
.register()
// Cache metrics
val cacheOperationsTotal: Counter = Counter.build()
.name("cache_operations_total")
.help("Total number of cache operations")
.labelNames("operation", "result")
.register()
val cacheHitRatio: Gauge = Gauge.build()
.name("cache_hit_ratio")
.help("Cache hit ratio")
.labelNames("cache_name")
.register()
// Error metrics
val errorsTotal: Counter = Counter.build()
.name("errors_total")
.help("Total number of errors")
.labelNames("error_type", "component")
.register()
def initialize(): Unit = {
// Register JVM metrics
DefaultExports.initialize()
// Initialize custom metrics
System.setProperty("io.prometheus.simpleclient.defaultMetrics", "true")
}
// Utility methods for timing operations
def timeOperation[T](histogram: Histogram, labels: String*)(operation: => T): T = {
val timer = histogram.labels(labels: _*).startTimer()
try {
operation
} finally {
timer.observeDuration()
}
}
def timeOperationAsync[T](histogram: Histogram, labels: String*)(operation: => Future[T])(implicit ec: ExecutionContext): Future[T] = {
val timer = histogram.labels(labels: _*).startTimer()
operation.andThen {
case _ => timer.observeDuration()
}
}
}
// Metrics service for Prometheus scraping
class MetricsService {
import akka.http.scaladsl.server.Directives._
val route = {
get {
complete {
val writer = new java.io.StringWriter()
TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples())
HttpEntity(ContentTypes.`text/plain(UTF-8)`, writer.toString)
}
}
}
}
// Custom metrics collector for business logic
class BusinessMetricsCollector extends Collector {
import scala.jdk.CollectionConverters._
override def collect(): java.util.List[Collector.MetricFamilySamples] = {
val samples = List(
new Collector.MetricFamilySamples(
"business_custom_metric",
Collector.Type.GAUGE,
"Custom business metric",
List(
new Collector.MetricFamilySamples.Sample(
"business_custom_metric",
List("label1").asJava,
List("value1").asJava,
42.0
)
).asJava
)
)
samples.asJava
}
}
// Application monitoring with structured logging
import ch.qos.logback.classic.Logger
import net.logstash.logback.marker.Markers
import org.slf4j.{LoggerFactory, MDC}
object ApplicationLogging {
private val logger = LoggerFactory.getLogger("application").asInstanceOf[Logger]
case class LogContext(
requestId: String,
userId: Option[String] = None,
operation: String,
component: String,
additionalFields: Map[String, String] = Map.empty
) {
def toMDC(): Unit = {
MDC.put("requestId", requestId)
userId.foreach(MDC.put("userId", _))
MDC.put("operation", operation)
MDC.put("component", component)
additionalFields.foreach { case (k, v) => MDC.put(k, v) }
}
def clearMDC(): Unit = {
MDC.clear()
}
}
def withContext[T](context: LogContext)(operation: => T): T = {
try {
context.toMDC()
operation
} finally {
context.clearMDC()
}
}
def logBusinessEvent(
event: String,
level: String = "INFO",
details: Map[String, Any] = Map.empty
): Unit = {
val marker = Markers.appendEntries(details.asJava)
level.toUpperCase match {
case "DEBUG" => logger.debug(marker, event)
case "INFO" => logger.info(marker, event)
case "WARN" => logger.warn(marker, event)
case "ERROR" => logger.error(marker, event)
case _ => logger.info(marker, event)
}
}
def logError(
message: String,
throwable: Throwable,
context: Map[String, Any] = Map.empty
): Unit = {
val marker = Markers.appendEntries(context.asJava)
logger.error(marker, message, throwable)
// Update error metrics
ApplicationMetrics.errorsTotal.labels(
throwable.getClass.getSimpleName,
context.getOrElse("component", "unknown").toString
).inc()
}
}
// Distributed tracing integration
import brave.{Tracing, Span}
import brave.sampler.Sampler
import zipkin2.reporter.AsyncReporter
import zipkin2.reporter.okhttp3.OkHttpSender
object DistributedTracing {
private lazy val sender = OkHttpSender.create("http://zipkin:9411/api/v2/spans")
private lazy val spanReporter = AsyncReporter.create(sender)
private lazy val tracing = Tracing.newBuilder()
.localServiceName("scala-production-app")
.spanReporter(spanReporter)
.sampler(Sampler.create(0.1f)) // Sample 10% of traces
.build()
def tracer = tracing.tracer()
def withSpan[T](operationName: String, tags: Map[String, String] = Map.empty)(operation: Span => T): T = {
val span = tracer.nextSpan().name(operationName).start()
try {
tags.foreach { case (key, value) => span.tag(key, value) }
operation(span)
} catch {
case ex: Throwable =>
span.tag("error", ex.getMessage)
throw ex
} finally {
span.end()
}
}
def shutdown(): Unit = {
tracing.close()
spanReporter.close()
sender.close()
}
}
CI/CD Pipeline Implementation
GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
Comments
Be the first to comment on this lesson!