Введение в serverless функции на Kotlin и AWS Lambda
Serverless-архитектура стала стандартом для создания масштабируемых и экономичных приложений. AWS Lambda — один из самых популярных сервисов для запуска кода без управления серверами. Однако большинство примеров и документации ориентированы на Java или Python. Kotlin, будучи современным языком для JVM, предлагает лаконичный синтаксис и полную совместимость с экосистемой Java, что делает его отличным выбором для Lambda-функций.
В этой статье мы разберём полный цикл: от настройки проекта до деплоя и мониторинга. Вы узнаете, как писать эффективные serverless функции на Kotlin, обрабатывать события API Gateway, S3 и DynamoDB, а также оптимизировать холодный старт.
1. Настройка проекта и зависимости
Для начала создадим Gradle-проект с поддержкой Kotlin. Основная зависимость — com.amazonaws:aws-lambda-java-core, которая предоставляет интерфейсы для хендлеров.
Шаг 1.1: Структура build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
id("io.github.microutils:kotlin-logging") version "3.0.5"
}
dependencies {
implementation("com.amazonaws:aws-lambda-java-core:1.2.2")
implementation("com.amazonaws:aws-lambda-java-events:3.11.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3")
implementation("org.slf4j:slf4j-simple:2.0.9")
}
tasks.register<Zip>("buildLambda") {
from(sourceSets.main.get().output)
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
into("build/libs")
archiveFileName.set("lambda.zip")
}
Обратите внимание на задачу buildLambda — она создаёт «толстый» JAR (fat jar), который AWS Lambda сможет исполнить. Без этого шага деплой не сработает.
Шаг 1.2: Базовая функция-хендлер
Создадим файл Handler.kt с простейшей функцией, которая принимает Map<String, String> и возвращает строку:
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
class HelloHandler : RequestHandler<Map<String, String>, String> {
override fun handleRequest(input: Map<String, String>, context: Context): String {
val name = input["name"] ?: "World"
context.logger.log("Processing request for $name")
return "Hello, $name!"
}
}
Интерфейс RequestHandler параметризован двумя типами: тип входных данных и тип возвращаемого значения. Kotlin позволяет использовать Map, String, а также любые data-классы.
2. Работа с событиями AWS (API Gateway, S3, DynamoDB)
Serverless-функции редко работают изолированно. Чаще всего они реагируют на события от других сервисов AWS. Рассмотрим три популярных сценария.
2.1 Обработка HTTP-запросов через API Gateway
Для интеграции с API Gateway используйте класс APIGatewayProxyRequestEvent из aws-lambda-java-events:
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
class ApiHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
override fun handleRequest(input: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent {
val body = "Received: ${input.body}"
return APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(body)
.withHeaders(mapOf("Content-Type" to "text/plain"))
}
}
В этом примере мы извлекаем тело запроса и возвращаем HTTP-ответ. Обратите внимание: APIGatewayProxyResponseEvent позволяет задать заголовки, статус-код и тело.
2.2 Реакция на загрузку файла в S3
События S3 передаются через S3Event. Типичная задача — извлечь метаданные загруженного объекта:
import com.amazonaws.services.lambda.runtime.events.S3Event
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
class S3EventHandler : RequestHandler<S3Event, String> {
override fun handleRequest(event: S3Event, context: Context): String {
event.records.forEach { record ->
val bucket = record.s3.bucket.name
val key = record.s3.`object`.key
context.logger.log("File $key uploaded to $bucket")
}
return "Processed ${event.records.size} records"
}
}
Обратите внимание на обратные кавычки вокруг object — это необходимо, так как object является зарезервированным словом в Kotlin.
2.3 Запись в DynamoDB
Для работы с DynamoDB используйте AWS SDK v2 для Kotlin. Добавьте зависимость:
implementation("software.amazon.awssdk:dynamodb:2.21.29")
Пример функции, которая сохраняет данные из события:
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
class DynamoDbHandler : RequestHandler<Map<String, String>, String> {
private val dynamoDb = DynamoDbClient.builder().build()
override fun handleRequest(input: Map<String, String>, context: Context): String {
val item = mapOf(
"id" to AttributeValue.builder().s(input["id"]).build(),
"data" to AttributeValue.builder().s(input["data"]).build()
)
dynamoDb.putItem(PutItemRequest.builder()
.tableName("MyTable")
.item(item)
.build())
return "Saved"
}
}
Важно: Клиент DynamoDbClient лучше создавать один раз на уровне класса (как показано выше), чтобы избежать повторного создания подключения при каждом вызове. Это снижает холодный старт.
3. Деплой и тестирование
После написания кода необходимо упаковать и загрузить функцию в AWS. Рассмотрим два способа: через AWS Console и через AWS CLI