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.