Kubernetes se consolidou como o padrao para orquestracao de containers, e aplicacoes Kotlin rodam excepcionalmente bem nesse ambiente. Com a JVM otimizada para containers e frameworks como Spring Boot e Ktor prontos para cloud-native, Kotlin e Kubernetes formam uma dupla poderosa. Vamos explorar como fazer isso na pratica.

Preparando a Aplicacao Kotlin para Kubernetes

Dockerfile Otimizado

O primeiro passo e criar uma imagem Docker eficiente:

// build.gradle.kts - Configuracao para gerar JAR otimizado
plugins {
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.spring") version "2.1.0"
    id("org.springframework.boot") version "3.3.0"
    id("io.spring.dependency-management") version "1.1.4"
}

tasks.bootJar {
    archiveFileName.set("app.jar")
    layerTools {
        enabled = true  // Habilita layer tools para build Docker eficiente
    }
}

// Configuracoes JVM para containers
tasks.bootRun {
    jvmArgs = listOf(
        "-XX:+UseContainerSupport",
        "-XX:MaxRAMPercentage=75.0",
        "-XX:InitialRAMPercentage=50.0"
    )
}

O Dockerfile multi-stage ideal para Spring Boot com Kotlin usa layer extraction para maximizar cache de camadas Docker. A primeira fase extrai as camadas do JAR, e a segunda fase copia cada camada separadamente, permitindo que o Docker reutilize cache para camadas que nao mudaram.

Health Checks para Kubernetes

Kubernetes usa probes para gerenciar o ciclo de vida dos pods:

// Health checks detalhados para Kubernetes
@RestController
class KubernetesHealthController(
    private val dataSource: DataSource
) {
    // Liveness probe - "A aplicacao esta viva?"
    // Se falhar, Kubernetes reinicia o pod
    @GetMapping("/health/live")
    fun liveness(): ResponseEntity<Map<String, String>> {
        return ResponseEntity.ok(mapOf("status" to "alive"))
    }

    // Readiness probe - "A aplicacao esta pronta para receber trafego?"
    // Se falhar, Kubernetes remove o pod do service
    @GetMapping("/health/ready")
    fun readiness(): ResponseEntity<Map<String, Any>> {
        val dbReady = try {
            dataSource.connection.use { it.isValid(2) }
        } catch (e: Exception) {
            false
        }

        return if (dbReady) {
            ResponseEntity.ok(mapOf(
                "status" to "ready",
                "database" to "connected"
            ))
        } else {
            ResponseEntity.status(HttpStatusCode.valueOf(503)).body(mapOf(
                "status" to "not_ready",
                "database" to "disconnected"
            ))
        }
    }

    // Startup probe - "A aplicacao terminou de iniciar?"
    // Util para aplicacoes JVM que demoram para iniciar
    @GetMapping("/health/started")
    fun startup(): ResponseEntity<Map<String, String>> {
        return ResponseEntity.ok(mapOf("status" to "started"))
    }
}

Graceful Shutdown

Kubernetes envia SIGTERM antes de matar um pod. Sua aplicacao precisa tratar isso:

// Configuracao de graceful shutdown
// application.yml:
// server:
//   shutdown: graceful
// spring:
//   lifecycle:
//     timeout-per-shutdown-phase: 30s

@Component
class GracefulShutdownHandler(
    private val processamentoService: ProcessamentoService
) {
    @PreDestroy
    fun onShutdown() {
        println("Recebido sinal de shutdown, finalizando processamentos...")
        runBlocking {
            processamentoService.finalizarPendentes()
        }
        println("Shutdown completado com sucesso")
    }
}

// Service que respeita shutdown
@Service
class ProcessamentoService {
    private val processando = AtomicBoolean(true)

    suspend fun finalizarPendentes() {
        processando.set(false)
        // Aguardar processamentos em andamento
        delay(5000)
    }

    suspend fun processar(item: Item) {
        if (!processando.get()) {
            throw ShutdownInProgressException()
        }
        // Processamento normal
    }
}

Configuracao do Kubernetes

Deployment

O Deployment define como sua aplicacao roda no Kubernetes. A configuracao inclui:

// Configuracao da aplicacao para ler do environment
@Configuration
@ConfigurationProperties(prefix = "app")
data class AppConfig(
    var databaseUrl: String = "",
    var databaseUsername: String = "",
    var databasePassword: String = "",
    var redisHost: String = "localhost",
    var redisPort: Int = 6379,
    var featureFlags: FeatureFlags = FeatureFlags()
) {
    data class FeatureFlags(
        var novaUI: Boolean = false,
        var cacheHabilitado: Boolean = true
    )
}

O manifesto Kubernetes correspondente define replicas, recursos (requests e limits de CPU e memoria), probes de saude, variaveis de ambiente via ConfigMap e Secrets, e estrategia de rolling update.

Configuracao de Recursos JVM

A JVM dentro de containers precisa de atencao especial:

// Configuracao JVM otimizada para Kubernetes
// Variavel de ambiente JAVA_OPTS no Deployment:
// -XX:+UseContainerSupport         -> Reconhece limits do container
// -XX:MaxRAMPercentage=75.0        -> Usa ate 75% da RAM disponivel
// -XX:InitialRAMPercentage=50.0    -> Inicia com 50% da RAM
// -XX:+UseG1GC                     -> Garbage Collector eficiente
// -XX:+UseStringDeduplication      -> Economia de memoria
// -Xss512k                         -> Stack size reduzido por thread

// Verificar configuracao em runtime
@PostConstruct
fun logJvmConfig() {
    val runtime = Runtime.getRuntime()
    logger.info("JVM Config - Max Memory: ${runtime.maxMemory() / 1024 / 1024}MB")
    logger.info("JVM Config - Available Processors: ${runtime.availableProcessors()}")
    logger.info("JVM Config - Total Memory: ${runtime.totalMemory() / 1024 / 1024}MB")
}

Service e Ingress

Para expor sua aplicacao, voce precisa de um Service e um Ingress. O Service abstrai o acesso aos pods, e o Ingress gerencia o roteamento HTTP externo com TLS.

Horizontal Pod Autoscaler (HPA)

Escale sua aplicacao automaticamente baseado em metricas:

// Exponha metricas customizadas para o HPA
@Component
class AutoscalingMetrics(
    private val meterRegistry: MeterRegistry,
    private val filaProcessamento: BlockingQueue<Tarefa>
) {
    init {
        // Metrica customizada para autoscaling
        meterRegistry.gauge("app.fila.tamanho", filaProcessamento) {
            it.size.toDouble()
        }

        meterRegistry.gauge("app.fila.utilizacao", filaProcessamento) {
            it.size.toDouble() / 1000.0  // Capacidade maxima = 1000
        }
    }
}

O HPA pode escalar baseado em CPU, memoria ou metricas customizadas via Prometheus Adapter.

Ktor no Kubernetes

Se voce usa Ktor em vez de Spring Boot, a configuracao e mais leve:

// Ktor otimizado para Kubernetes
fun main() {
    embeddedServer(
        Netty,
        port = System.getenv("PORT")?.toIntOrNull() ?: 8080,
        host = "0.0.0.0"
    ) {
        install(ContentNegotiation) {
            json()
        }

        routing {
            // Health checks
            get("/health/live") {
                call.respond(mapOf("status" to "alive"))
            }

            get("/health/ready") {
                val healthy = checkDependencies()
                if (healthy) {
                    call.respond(mapOf("status" to "ready"))
                } else {
                    call.respond(
                        HttpStatusCode.ServiceUnavailable,
                        mapOf("status" to "not_ready")
                    )
                }
            }

            // API routes
            route("/api/v1") {
                produtoRoutes()
                pedidoRoutes()
            }
        }
    }.start(wait = true)
}

// Shutdown hook para Ktor
Runtime.getRuntime().addShutdownHook(Thread {
    println("Shutdown hook executado")
    // Cleanup resources
})

Logging para Kubernetes

Em Kubernetes, logs devem ir para stdout/stderr em formato JSON:

// Configuracao de logging para Kubernetes
// Use JSON format para que ferramentas como Fluentd ou Loki
// possam processar os logs automaticamente

// logback-spring.xml para producao deve usar:
// - ConsoleAppender com LogstashEncoder
// - Nao usar file appenders (Kubernetes gerencia logs)
// - Incluir campos: timestamp, level, logger, message, stack_trace

// Exemplo de log que fica bem em Kubernetes
logger.info("Pedido processado",
    StructuredArguments.kv("pedido_id", pedido.id),
    StructuredArguments.kv("duracao_ms", duracao),
    StructuredArguments.kv("status", "sucesso")
)
// Resultado em JSON:
// {"timestamp":"2025-08-24T10:00:00","level":"INFO",
//  "message":"Pedido processado","pedido_id":"abc-123",
//  "duracao_ms":42,"status":"sucesso"}

Secrets e ConfigMaps

Gerenciar configuracao sensivel em Kubernetes:

// Lendo secrets do Kubernetes como variaveis de ambiente
@Configuration
class SecretsConfig {
    @Value("\${DB_PASSWORD}")
    private lateinit var dbPassword: String

    @Value("\${API_KEY}")
    private lateinit var apiKey: String

    // Ou usando ConfigurationProperties
}

Para segredos mais sensiveis, considere usar External Secrets Operator ou HashiCorp Vault com integracao Kubernetes.

Boas Praticas

  1. Imagens pequenas: Use JRE alpine, nao JDK completo
  2. Limits realistas: Defina CPU e memoria baseados em testes de carga
  3. Health checks: Sempre configure liveness, readiness e startup probes
  4. Graceful shutdown: Trate SIGTERM adequadamente
  5. 12-Factor App: Configuracao via environment variables
  6. Observabilidade: Logs JSON, metricas Prometheus, tracing OpenTelemetry
  7. Security context: Rode containers como non-root

Conclusao

Kotlin e Kubernetes formam uma combinacao poderosa para aplicacoes cloud-native. A JVM moderna e otimizada para containers, frameworks como Spring Boot e Ktor tem suporte nativo para Kubernetes, e as ferramentas de observabilidade da JVM sao as mais maduras do mercado.

Invista em entender bem o ecossistema Kubernetes e como configurar sua aplicacao Kotlin para aproveitar ao maximo a plataforma. O resultado sera aplicacoes resilientes, escalaveis e faceis de operar em producao.