AWS Lambda – це сервіс безсерверних обчислень від Amazon. Це платформа для виконання коду без необхідності управління серверами. Цей сервіс надає можливість розробляти адаптивні програмні рішення, що активуються різними подіями. Завдяки своїй архітектурі без серверів Lambda дає змогу розробникам концентруватися виключно на написанні програмної логіки, тоді як Amazon самостійно відповідає за автоматичне масштабування та підтримку інфраструктури. Сервіс відзначається вигідною моделлю оплати – кошти стягуються виключно за період активної роботи коду та використаний обсяг оперативної пам’яті.
В рамках даного матеріалу ми вивчимо процес розробки простої Lambda-функції з використанням мови програмування Java. Матеріал охоплює огляд потрібних бібліотек, альтернативні підходи до створення функції, формування пакету для розгортання, а також методи локального тестування. Для успішного проходження цього матеріалу бажано мати діючий аккаунт в AWS.
1. Dependencies
Давайте для початку подивимося які залежності нам потрібні:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-lambda</artifactId>
<version>1.10.35</version>
</dependency>XMLТакож не забудьте додати цей плагін:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>XMLMaven Shade Plugin необхідний при створенні функцій AWS Lambda за допомогою Java. Він дозволяє нам упакувати наш додаток і його залежності в єдиний, автономний JAR-файл, також відомий як fat JAR. Плагін витягує вміст усіх наших залежностей і поміщає їх разом з класами нашого проекту – саме так AWS Lambda очікує від нас розгортання нашого коду.
Ми можемо створити наш JAR у цільовому каталозі нашого проекту, запустивши його командою:
mvn clean packageXMLЯкщо ви використовуєте Gradle то можете створити единий fat JAR за допомогою плагіна Gradle Shadow Plugin.
2. Створення handler
Точкою входу для будь-якої лямбда-функції AWS є метод handler. Він обробляє вхідний запит і повертає відповідь. При створенні lambda function ми повинні вказати наш обробник. Ми робимо це за допомогою формату package.ClassName. Ми розглянемо, як вказати цю конфігурацію в наступних розділах, де ми будемо тестувати і розгортати нашу лямбда-функцію.
У нас є кілька різних варіантів визначення методу-обробника, і ми розглянемо їх у цьому розділі.
2.1. Реалізація інтерфейсу RequestHandler
Найбільш поширеним і рекомендованим способом визначення обробника для AWS lambda є реалізація інтерфейсу RequestHandler та перевизначення його методу handleRequest():
class LambdaHandler implements RequestHandler<Request, Response> {
@Override
public Response handleRequest(Request request, Context context) {
LambdaLogger logger = context.getLogger();
logger.log("Processing question from " + request.name(), LogLevel.INFO);
return new Response("Processing: java.com.ua/method");
}
}
record Request(String name, String question) {}
record Response(String answer) {}JavaRequest і response – це прості записи, які представляють вхідні та вихідні дані нашої лямбда-функції. Ми також вказуємо ці типи як загальні параметри в інтерфейсі RequestHandler.
Метод handleRequest() приймає наш запис Request і об’єкт Context як параметри. Параметр Context містить корисну інформацію про середовище виконання Lambda, зокрема про LambdaLogger, який ми можемо використовувати для ведення журналу.
2.2. Реалізація інтерфейсу RequestStreamHandler
Іншим підходом до визначення обробника є реалізація інтерфейсу RequestStreamHandler:
class LambdaStreamHandler implements RequestStreamHandler {
@Override
public void handleRequest(InputStream input, OutputStream output, Context context) {
ObjectMapper mapper = new ObjectMapper();
Request request = mapper.readValue(input, Request.class);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output))) {
writer.write("Hello " + request.name());
writer.flush();
}
}
record Request(String name) {}
}JavaТут ми реалізуємо інтерфейс RequestStreamHandler і перевизначаємо метод handleRequest(). Використовуючи ObjectMapper, ми десеріалізуємо необроблені дані запиту з InputStream і записуємо нашу відповідь у OutputStream. Цей інтерфейс корисний при роботі з необробленими вхідними та вихідними потоками.
2.3. Custom Handler Method
Нарешті, ми можемо визначити власний custom метод обробника:
class CustomLambdaHandler {
public Response handlingRequestFreely(Request request, Context context) {
LambdaLogger logger = context.getLogger();
logger.log(request.name() + " has invoked the lambda function", LogLevel.INFO);
return new Response("Processing: java.com.ua/customMethod");
}
record Request(String name) {}
record Response(String answer) {}
}JavaВ даному варіанті ми формуємо клас CustomLambdaHandler з методом handlingRequestFreely(), який приймає об’єкт Request та Context як аргументи, що схоже на приклад з RequestHandler. Єдина відмінність полягає в тому, що ми не реалізуємо жодного конкретного інтерфейсу.
На відміну від двох попередніх методів, при розробці власного обробника потрібно застосовувати шаблон package.ClassName::methodName для налаштування нашого обробника. Наприклад, якщо наш клас CustomLambdaHandler розташований у пакеті com.java.lambda, тоді при створенні Lambda-функції ми вкажемо обробник як com.java.lambda.CustomLambdaHandler::handlingRequestFreely.
3. Тестування lambda function
Ми протестуємо наш клас LambdaHandler, який ми створили раніше, реалізувавши інтерфейс RequestHandler.
Спочатку запустимо контейнер LocalStack за допомогою Docker:
docker run \
--rm -it \
-p 127.0.0.1:4566:4566 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ./target:/opt/code/local/target \
local/localGroovyМи мапуємо потрібний порт і монтуємо цільову директорію нашого проекту, яка містить наш жирний JAR, в контейнер. Важливо зазначити, що є й інші способи встановлення Localstack.
Далі ми увійдемо в оболонку нашого контейнера і створимо нашу лямбда-функцію:
awslocal lambda create-function \
--function-name java-lambda-function \
--runtime java21 \
--handler com.java.lambda.LambdaHandler\
--role arn:aws:iam::000000000000:role/lambda-role \
--zip-file fileb:///opt/code/local/target/java-lambda-function-0.0.1.jarGroovyМи вказуємо наприклад Java 21 як середовище виконання, наш обробник і розташування нашого JAR-файлу в контейнері за допомогою параметра zip-файл.
Створивши нашу функцію, давайте викличемо її:
awslocal lambda invoke \
--function-name java-lambda-function \
--payload '{ "name": "Sofiya", "question": "What is your best toy and how can you like it?" }' output.txtGroovyМи передаємо ім’я нашої функції та корисне навантаження JSON-запиту. Відповідь від нашої лямбда-функції зберігається у вказаному файлі output.txt, який містить:
{
"answer": "Processing: java.com.ua/method"
}Groovy4. Виконуємо Lambda Function
Після успішного розгортання нашої лямбда-функції ми можемо викликати її через AWS CLI:
aws lambda invoke --function-name my-lambda-function \
--cli-binary-format raw-in-base64-out \
--payload '{ "name": "Sofiya", "question": "What is your best toy and how can you like it?" }' output.txtGroovyУ реальних умовах замість прямого виклику наших лямбда-функцій через CLI, наші лямбда-функції зазвичай запускаються подіями від різних сервісів AWS, таких як API Gateway і S3. Інший варіант використання – запуск лямбда-функцій наприклад за допомогою подій S3.
Однією з головних проблем при використанні Java для лямбда-функцій для додатків, чутливих до часу, є час холодного запуску. Коли лямбда-функція викликається після періоду бездіяльності, відбувається затримка запуску JVM і завантаження необхідних класів. Ці накладні витрати збільшують час, необхідний для завершення роботи нашої лямбда-функції. GraalVM може використовуватися як потенційне рішення для вирішення цієї проблеми. Він компілює наш Java-додаток у нативні виконувані файли, значно скорочуючи час запуску та оптимізуючи пам’ять.





