AWS Lambda e o servico serverless mais usado no mundo, e Kotlin e uma escolha excelente para criar funcoes Lambda robustas e performaticas. Neste artigo, vamos explorar como desenvolver, otimizar e fazer deploy de funcoes Lambda com Kotlin.
Por Que Kotlin no AWS Lambda
Kotlin traz vantagens significativas para desenvolvimento serverless:
- Null safety: Reduz erros em runtime, criticos em ambientes serverless onde debugging e mais dificil
- Concisao: Menos codigo significa menos bugs e deploy mais rapido
- Ecossistema JVM: Acesso a todas as bibliotecas Java, incluindo o AWS SDK
- Coroutines: Gerenciamento eficiente de operacoes assincronas
O principal desafio historico de usar JVM no Lambda era o cold start. Com otimizacoes recentes (SnapStart, GraalVM, Kotlin/Native), esse problema esta cada vez menor.
Primeira Funcao Lambda com Kotlin
Setup do Projeto
// build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
id("com.github.johnrengelman.shadow") version "8.1.1" // Fat JAR
}
dependencies {
implementation("com.amazonaws:aws-lambda-java-core:1.2.3")
implementation("com.amazonaws:aws-lambda-java-events:3.11.4")
implementation("software.amazon.awssdk:dynamodb:2.25.0")
implementation("software.amazon.awssdk:s3:2.25.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.13.9")
}
tasks.shadowJar {
archiveClassifier.set("")
mergeServiceFiles()
}
Handler Basico
// Handler para API Gateway
class ProdutoHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private val produtoService = ProdutoService()
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
override fun handleRequest(
input: APIGatewayProxyRequestEvent,
context: Context
): APIGatewayProxyResponseEvent {
context.logger.log("Recebida requisicao: ${input.httpMethod} ${input.path}")
return try {
when (input.httpMethod) {
"GET" -> handleGet(input)
"POST" -> handlePost(input)
else -> response(405, mapOf("error" to "Metodo nao permitido"))
}
} catch (e: Exception) {
context.logger.log("Erro: ${e.message}")
response(500, mapOf("error" to "Erro interno"))
}
}
private fun handleGet(input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent {
val id = input.pathParameters?.get("id")
return if (id != null) {
val produto = produtoService.buscarPorId(id)
if (produto != null) {
response(200, produto)
} else {
response(404, mapOf("error" to "Produto nao encontrado"))
}
} else {
val produtos = produtoService.listarTodos()
response(200, produtos)
}
}
private fun handlePost(input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent {
val request = json.decodeFromString<CriarProdutoRequest>(input.body)
val produto = produtoService.criar(request)
return response(201, produto)
}
private fun <T> response(statusCode: Int, body: T): APIGatewayProxyResponseEvent {
return APIGatewayProxyResponseEvent().apply {
this.statusCode = statusCode
this.headers = mapOf(
"Content-Type" to "application/json",
"Access-Control-Allow-Origin" to "*"
)
this.body = json.encodeToString(
serializer = kotlinx.serialization.serializer(),
value = body
)
}
}
}
Integracao com DynamoDB
DynamoDB e o banco de dados mais usado com Lambda:
// Service para DynamoDB com Kotlin
class ProdutoService {
private val dynamoDb = DynamoDbClient.builder()
.region(Region.SA_EAST_1)
.build()
private val tableName = System.getenv("PRODUTOS_TABLE") ?: "produtos"
private val json = Json { ignoreUnknownKeys = true }
fun buscarPorId(id: String): Produto? {
val request = GetItemRequest.builder()
.tableName(tableName)
.key(mapOf("id" to AttributeValue.builder().s(id).build()))
.build()
val response = dynamoDb.getItem(request)
return if (response.hasItem()) {
response.item().toProduto()
} else null
}
fun criar(request: CriarProdutoRequest): Produto {
val produto = Produto(
id = java.util.UUID.randomUUID().toString(),
nome = request.nome,
preco = request.preco,
categoria = request.categoria,
criadoEm = java.time.Instant.now().toString()
)
val item = mapOf(
"id" to AttributeValue.builder().s(produto.id).build(),
"nome" to AttributeValue.builder().s(produto.nome).build(),
"preco" to AttributeValue.builder().n(produto.preco.toString()).build(),
"categoria" to AttributeValue.builder().s(produto.categoria).build(),
"criadoEm" to AttributeValue.builder().s(produto.criadoEm).build()
)
dynamoDb.putItem(
PutItemRequest.builder()
.tableName(tableName)
.item(item)
.build()
)
return produto
}
fun listarTodos(): List<Produto> {
val response = dynamoDb.scan(
ScanRequest.builder()
.tableName(tableName)
.limit(100)
.build()
)
return response.items().map { it.toProduto() }
}
private fun Map<String, AttributeValue>.toProduto(): Produto {
return Produto(
id = this["id"]!!.s(),
nome = this["nome"]!!.s(),
preco = this["preco"]!!.n().toDouble(),
categoria = this["categoria"]!!.s(),
criadoEm = this["criadoEm"]!!.s()
)
}
}
@Serializable
data class Produto(
val id: String,
val nome: String,
val preco: Double,
val categoria: String,
val criadoEm: String
)
@Serializable
data class CriarProdutoRequest(
val nome: String,
val preco: Double,
val categoria: String
)
Otimizando Cold Start
O cold start e o maior desafio de JVM no Lambda. Aqui estao estrategias para minimiza-lo:
AWS SnapStart
SnapStart cria um snapshot da JVM apos a inicializacao, eliminando o cold start em invocacoes subsequentes:
// Para usar SnapStart, adicione ao template SAM:
// Properties:
// SnapStart:
// ApplyOn: PublishedVersions
// Otimize o codigo para SnapStart
class OptimizedHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
// Inicialize clientes no companion object (cached entre invocacoes)
companion object {
private val dynamoDb = DynamoDbClient.builder()
.region(Region.SA_EAST_1)
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build()
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
}
override fun handleRequest(
input: APIGatewayProxyRequestEvent,
context: Context
): APIGatewayProxyResponseEvent {
// Handler logic usando clientes pre-inicializados
return processarRequisicao(input, context)
}
private fun processarRequisicao(
input: APIGatewayProxyRequestEvent,
context: Context
): APIGatewayProxyResponseEvent {
// ...processar
return APIGatewayProxyResponseEvent().apply {
statusCode = 200
body = "{\"status\": \"ok\"}"
}
}
}
Reducao de Dependencias
Cada dependencia adiciona tempo ao cold start:
// Use apenas o que precisa do AWS SDK v2
dependencies {
// Em vez de importar o SDK inteiro:
// implementation("software.amazon.awssdk:aws-sdk-java:2.25.0") // NAO!
// Importe apenas os modulos necessarios:
implementation("software.amazon.awssdk:dynamodb:2.25.0")
implementation("software.amazon.awssdk:url-connection-client:2.25.0") // HTTP client leve
}
Configuracao de Memoria
Mais memoria no Lambda significa mais CPU, o que acelera o cold start:
// Regra pratica para Kotlin Lambda:
// - Minimo 512MB para funcoes simples
// - 1024MB para funcoes com mais dependencias
// - 2048MB+ para funcoes com muitas dependencias ou processamento pesado
// A relacao custo-beneficio costuma ser otima com 1024MB
// Funcoes rodam mais rapido e o custo total pode ser MENOR
Processamento de Eventos
Lambda nao e apenas para APIs. Processar eventos e um caso de uso poderoso:
// Processador de eventos SQS
class SqsEventHandler : RequestHandler<SQSEvent, Void?> {
private val pedidoProcessor = PedidoProcessor()
override fun handleRequest(event: SQSEvent, context: Context): Void? {
val logger = context.logger
event.records.forEach { record ->
try {
logger.log("Processando mensagem: ${record.messageId}")
val pedido = Json.decodeFromString<PedidoEvento>(record.body)
pedidoProcessor.processar(pedido)
logger.log("Mensagem processada com sucesso: ${record.messageId}")
} catch (e: Exception) {
logger.log("Erro ao processar mensagem ${record.messageId}: ${e.message}")
throw e // Re-throw para SQS retry
}
}
return null
}
}
// Processador de eventos S3
class S3EventHandler : RequestHandler<S3Event, String> {
override fun handleRequest(event: S3Event, context: Context): String {
val record = event.records.first()
val bucket = record.s3.bucket.name
val key = record.s3.`object`.key
context.logger.log("Novo arquivo: s3://$bucket/$key")
// Processar arquivo
val s3Client = S3Client.builder().region(Region.SA_EAST_1).build()
val response = s3Client.getObject(
GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build()
)
val conteudo = response.readAllBytes().decodeToString()
// Processar conteudo...
return "Processado: $key"
}
}
Testes Locais
Testar Lambda localmente acelera o desenvolvimento:
// Testes unitarios para Lambda handlers
class ProdutoHandlerTest {
private val handler = ProdutoHandler()
@Test
fun `deve retornar produto quando existe`() {
val event = APIGatewayProxyRequestEvent().apply {
httpMethod = "GET"
pathParameters = mapOf("id" to "prod-123")
}
val context = mockk<Context> {
every { logger } returns mockk(relaxed = true)
}
val response = handler.handleRequest(event, context)
assertEquals(200, response.statusCode)
assertTrue(response.body.contains("prod-123"))
}
@Test
fun `deve retornar 404 quando produto nao existe`() {
val event = APIGatewayProxyRequestEvent().apply {
httpMethod = "GET"
pathParameters = mapOf("id" to "inexistente")
}
val context = mockk<Context> {
every { logger } returns mockk(relaxed = true)
}
val response = handler.handleRequest(event, context)
assertEquals(404, response.statusCode)
}
}
Conclusao
Kotlin no AWS Lambda e uma combinacao madura e pronta para producao. Com as otimizacoes certas, especialmente SnapStart e gerenciamento cuidadoso de dependencias, o cold start deixa de ser um problema significativo. A combinacao de null safety, coroutines e o ecossistema JVM torna Kotlin uma das melhores linguagens para desenvolvimento serverless robusto.
Comece com funcoes simples, otimize gradualmente e aproveite todo o poder do ecossistema Kotlin no mundo serverless. Com a AWS expandindo constantemente o suporte a JVM, o futuro do Kotlin no Lambda e promissor.