AWS Lambda in Action на Java 11. Заезжаем с Serverless в «Production»

Статья — гайд о том, как быстро и без боли начать использовать AWS Lambda на простом примере. Подойдет, как разработчику, не работавшему с Lambda вовсе, так и познавшему Cloud, чтобы оценить еще одно видение на разработку Serverless приложений.

image


Всем привет.
Меня зовут Александр Груздев, я Java Team Lead в компании DINS. Уже больше двух лет я тесно работаю с AWS-инфраструктурой и имею опыт как в написании приложений под AWS, так и в деплойменте этих самых приложений. В своей работе мне приходилось использовать ElasticBeanstalk, ECS, Fargate, EKS, ну и, конечно, AWS Lambda.

Каждый из этих сервисов хорош по-своему, и в рамках этой статьи я не буду призывать использовать только AWS Lambda как замену всем остальным вариантам. Я лишь хочу показать, как легко начать использовать Lambda, и в каких случаях вы получите преимущества от ее использования.

Все, начиная от разработчиков и заканчивая менеджерами, хотят иметь четкий процесс доставки изменений клиентам. Чем более прозрачен этот путь и чем меньше усилий требуется — тем выгоднее. Чаще всего разработчики и тестировщики не хотят знать, куда будет деплоиться приложение, какую железку выбрать, в какой регион нужно поставить реплики и т.д. На следующей диаграмме можно увидеть услуги класса «aaS» (as a service), где AWS Lambda представляет категорию FaaS.

image

FaaS в двух словах

FaaS значит, что вы можете написать небольшую функцию и использовать ее как часть приложения. То есть вы можете разделить, к примеру, CRUD приложение на C, R, U, D и по отдельности реализовать Create функционал и Read. Это не всегда будет работать эффективнее, но это — лишь пример.


Чтобы продемонстрировать, что представляет собой разработка Serverless-решения, я решил взять достаточно банальный пример. Думаю, вы на многих сайтах видели форму обратной связи Contact Us, где можно оставить ваши почту или телефон и задать вопрос, чтобы вам позже ответили. Скрин ниже, форма не супер, но и сабж данной статьи — не material design.
gv5givju7glj8vftkb1cdhupdiw.png

Заполнив эту форму, потенциальный клиент будет ждать звонка или письма от вас.
С технической стороны вам нужно:

  • сохранить данные из формы в базу;
  • отправить письмо агенту, который будет отвечать на вопрос;
  • возможно, написать еще сервис, где агент будет отмечать сделанную работу на основе той же базы (не входит в рамки статьи).

Реализация идеи доступна в GitHub.


Первое, что приходит в голову при реализации, то что нам нужны:

  • машина под back, front, images и т. д.,
  • машина с базой,
  • машина с email-сервисом.

9a7h83iyieeicpht4-bcwy8l-ja.png

Бэк и базу, по-хорошему, нужно реплицировать, но предположим, что мы это делаем для своего мини-магазина и нагрузка на эту форму минимальная. Поэтому все эти машины можно совместить в одну и деплоить единым набором.
Но так как мы рассматриваем Serverless, давайте построим архитектуру полностью на бессерверных компонентах.
Как бы многообещающе ни звучало Serverless, все мы понимаем, что сервера никуда не делись, они лишь скрылись из вашего поля зрения. И теперь ту рутинную работу, что вы могли делать самостоятельно, взял на себя ваш облачный провайдер. Но давайте все же попробуем использовать только такие бессерверные компоненты:
mfiw6if-6nlss4pl-brkgxenxuc.png

На диаграмме присутствует очень четкое разделение по функциональности каждого компонента.

  • AWS Lambda — главный компонент, который содержит код/логику,
  • S3 отвечает за хранение статических ресурсов и JS-скриптов,
  • CloudFront — за механизмы кеширования и мультирегиональную поддержку,
  • SES — email-сервис,
  • DynamoDB — база для хранения данных с формы (от кого, какой вопрос, куда отправить ответ),
  • API Gateway — HTTP API для нашей лямбды,
  • Route53 — понадобится, если вы захотите добавить красивое доменное имя.

Не все эти компоненты мы будем использовать в нашем последующем гайде, просто чтобы не растягивать статью.
Route53 и CloudFront — достаточно простые компоненты, про которые можно почитать отдельно.
Немножко спойлеров, что нам даст такое решение:

  • Мы отходим от поддержки EC2-машин, никакого дебага через ssh,
  • Легкая конфигурация: настраиваем троттлинг / кеширование / доступы в один клик,
  • Поддерживает access policies: ограничиваем права и даем доступ,
  • Логгирование / мониторинг из коробки,
  • Платите только за использованные ресурсы / реквесты.


Подготовка


Чтобы начать разработку нашего Serverless-решения, необходимо выполнение следующих требований:

После установки всех вышеперечисленных тулов нужно сконфигурировать ваш aws-cli для удаленного доступа к AWS. Следуйте инструкции. Это потребует создать нового пользователя и выгрузить его Access Key и Secret Key.

Сборка проекта


1. Создаем проект из шаблона


Откройте директорию для будущего проекта и из нее запустите SAM CLI. Следуйте инструкции:
kngu7fr3q_h_7t7bq-ygr7baccs.png

Заметка: В зависимости от версии SAM-CLI команды могут немного отличаться, но все они остаются интуитивно понятными. Просто выбирайте наиболее похожие на те, что были использованы выше. Также можете выбрать другой тул для сборки если Gradle вам не подходит.

Проект «Hello, World!» готов. Теперь можно поработать над названием проекта и пакетов, зависимостями и исходным кодом.

2. Займемся зависимостями


Добавьте в build.gradle следующие зависимости:

dependencies {
    // AWS
    implementation group: 'com.amazonaws.serverless', name: 'aws-serverless-java-container-core', version: '1.4'
    implementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.0'
    implementation group: 'com.amazonaws', name: 'aws-java-sdk-ses', version: '1.11.670'
    implementation group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.670'
    implementation group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.1.0'

    // Utils
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.10.0'
    implementation group: 'commons-io', name: 'commons-io', version: '2.6'

    // Test
    testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0'
    testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.4'
    testImplementation group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.4'
    testImplementation group: 'junit', name: 'junit', version: '4.12'
}


Основные — это AWS SDK. Они позволят работать с конкретными сервисами, такими как SES, DynamoDB и т.д.

3. Пишем лямбду


  • Меняем шаблонные реквест- и респонс-классы для RequestHandler на AwsProxyRequest и ContactUsProxyResponse.
    public class App implements RequestHandler
    ...
    public ContactUsProxyResponse handleRequest(AwsProxyRequest request, Context context) 

  • Добавляем класс AwsClientFactory для инициализации AWS SDK-клиентов.
    /**
     * Just an util class for an eager initialization of sdk clients.
     */
    public class AwsClientFactory {
    
        private static final Logger LOG = LogManager.getLogger(AwsClientFactory.class);
    
        private final AmazonSimpleEmailService sesClient;
        private final DynamoDB dynamoDB;
    
        /**
         * AWS regions should be env variables if you want to generalize the solution.
         */
        AwsClientFactory() {
            LOG.debug("AWS clients factory initialization.");
            sesClient = AmazonSimpleEmailServiceClient.builder().withRegion(Regions.EU_WEST_1).build();
            AmazonDynamoDB dynamoDBClient = AmazonDynamoDBClientBuilder.standard().withRegion(Regions.EU_WEST_1).build();
            dynamoDB = new DynamoDB(dynamoDBClient);
        }
    
        DynamoDB getDynamoDB() {
            return dynamoDB;
        }
    
        AmazonSimpleEmailService getSesClient() {
            return sesClient;
        }
    
    }
    

  • Инициализируем нашу фактори и ObjectMapper как статичные переменные.
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final AwsClientFactory AWS_CLIENT_FACTORY = new AwsClientFactory();
    

  • Реализуем логику по отправке емейлов и сохранению метадаты в базу.
    
     private SendEmailResult sendEmail(ContactUsRequest contactUsRequest) {
            String emailTemplate = getEmailTemplate();
            String email = fillTemplate(emailTemplate, contactUsRequest);
    
            SendEmailRequest sendEmailRequest =
                    new SendEmailRequest(
                            System.getenv("SENDER_EMAIL"),
                            new Destination(List.of(System.getenv("RECIPIENT_EMAIL"))),
                            new Message()
                                    .withSubject(
                                            new Content()
                                                    .withCharset(UTF_8.name())
                                                    .withData(contactUsRequest.getSubject()))
                                    .withBody(new Body()
                                            .withHtml(new Content()
                                                    .withCharset(UTF_8.name())
                                                    .withData(email))));
            LOG.info("Email template is ready");
            return AWS_CLIENT_FACTORY.getSesClient().sendEmail(sendEmailRequest);
    }
    
    private String fillTemplate(String emailTemplate, ContactUsRequest contactUsRequest) {
            return String.format(
                    emailTemplate,
                    contactUsRequest.getUsername(),
                    contactUsRequest.getEmail(),
                    contactUsRequest.getPhone(),
                    contactUsRequest.getQuestion());
    }
    
    private String getEmailTemplate() {
            try {
                return IOUtils.toString(
                        Objects.requireNonNull(this.getClass().getClassLoader()
                                                   .getResourceAsStream("email_template.html")),
                        UTF_8);
            } catch (IOException e) {
                throw new RuntimeException("Loading an email template failed.", e);
            }
    }
    
    private void addEmailDetailsToDb(ContactUsRequest contactUsRequest, SendEmailResult sendEmailResult) {
            AWS_CLIENT_FACTORY.getDynamoDB().getTable("ContactUsTable")
                              .putItem(new Item()
                                      .withPrimaryKey("Id", sendEmailResult.getMessageId())
                                      .withString("Subject", contactUsRequest.getSubject())
                                      .withString("Username", contactUsRequest.getUsername())
                                      .withString("Phone", contactUsRequest.getPhone())
                                      .withString("Email", contactUsRequest.getEmail())
                                      .withString("Question", contactUsRequest.getQuestion()));
    }
    

  • Собираем ответ на запрос со статус кодом и телом ответа.
    
    private ContactUsProxyResponse buildResponse(int statusCode, String body) {
            ContactUsProxyResponse awsProxyResponse =
                    new ContactUsProxyResponse();
            awsProxyResponse.setStatusCode(statusCode);
            awsProxyResponse.setBody(getBodyAsString(body));
            awsProxyResponse.addHeader(CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
            awsProxyResponse.addHeader("Access-Control-Allow-Origin", "*");
            return awsProxyResponse;
    }
    
     private String getBodyAsString(String body) {
            try {
                return OBJECT_MAPPER.writeValueAsString(new ContactUsResponseBody(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Writing ContactUsResponseBody as string failed.", e);
            }
    }
    

  • Добавляем логику для прогрева лямбды. В коде блок помечен как »// WARMING UP».
    
    if (Optional.ofNullable(request.getMultiValueHeaders()).map(headers -> headers.containsKey("X-WARM-UP")).orElse(FALSE)) {
                LOG.info("Lambda was warmed up");
                return buildResponse(201, "Lambda was warmed up. V1");
    }
    

  • Собираем все промежуточные вызовы в имплементацию основного метода лямбды handleRequest ()
    
    @Override
    public ContactUsProxyResponse handleRequest(AwsProxyRequest request, Context context) {
            LOG.info("Request was received");
            LOG.debug(getAsPrettyString(request));
    
            if (Optional.ofNullable(request.getMultiValueHeaders()).map(headers -> headers.containsKey("X-WARM-UP")).orElse(FALSE)) {
                LOG.info("Lambda was warmed up");
                return buildResponse(201, "Lambda was warmed up. V1");
            }
    
            ContactUsRequest contactUsRequest = getContactUsRequest(request);
    
            SendEmailResult sendEmailResult = sendEmail(contactUsRequest);
            LOG.info("Email was sent");
    
            addEmailDetailsToDb(contactUsRequest, sendEmailResult);
            LOG.info("DB is updated");
    
            return buildResponse(200,
                    String.format("Message %s has been sent successfully.", sendEmailResult.getMessageId()));
    }
    

Основная логика достаточно простая, поэтому не думаю, что стоит детально ее описывать. Есть несколько моментов, на которые стоит обратить внимание.

Первый — логгирование. Один из аргументов метода лямбды типа Context содержит в себе очень много служебной информации, а также логгер. Поэтому можно не создавать отдельный логгер, а использовать предоставляемый лямбда-контекстом. Для этого достаточно перед использованием вызвать:

LambdaLogger logger = context.getLogger();

Второй момент — прогрев. Так как создание выполняемого окружения для лямбды — ленивое, то старт JVM, загрузка classpath и выполнение кода требует некоторого времени. Первый вызов может занять несколько секунд, что не годится в случае написания различных синхронных API. Для таких случаев вы сами можете подсказать AWS, что нужно держать в боевой готовности несколько инстансов лямбды. Но это требует, чтобы кто-то вызвал лямбду. Если мы сделаем это с базовой версией кода, то, по сути, отправим письмо и запишем в базу какие-то недостоверные данные.
Чтобы этого избежать, мы можем добавить некую обработку запроса, чтобы отличить реальный запрос от запроса на прогрев. К примеру, можем добавлять специальный заголовок в запрос. В нашем случае будет использоваться заголовок «X-WARM-UP» с любым значением — чтобы понимать, что это запрос на прогрев и нам нужно лишь вернуть какой-то ответ без выполнения бизнес-логики.

Последнее, на что хотел бы обратить внимание, — использование статических переменных. Это не делает инстанс лямбда stateful в нашем случае, просто позволяет переиспользовать уже существующие и проинициализированные объекты. Если объекты, что вы хотите проинициализировать статично, изменяются во время выполнения кода — постарайтесь еще раз подумать, не помешает ли это работе последующих вызовов лямбды. Также, если эти объекты используют переменные окружения при инициализации, не стоит их делать статичными.

4. Пишем тесты


Код лямбды может быть покрыт теми же типами тестов, что вы используете в большинстве случаев. Но, чтобы статья не разрасталась еще больше, я рассмотрю тестирование Serverless приложений в следующей статье, посвященной тестированию и локальной разработке AWS Lambda. Хотя юнит тесты уже доступны в репозитории.

5. Пишем шаблон SAM-ресурсов


После того как мы написали и протестили нашу лямбду локально, нужно написать SAM-шаблон, чтобы задеплоить все эти ресурсы в AWS.

Немного о SAM
Serverless Application Model — это фреймворк для описание serverless ресурсов. Если коротко, то это некая обертка над CloudFormation DSL, которая упрощает описание ресурсов — таких как Lambda, Gateway, DynamoDB и т. д.
SAM, CloudFormation, Terraform — все это IaC-решения. SAM больше подходит для небольших Serverless-решений, тогда как Terraform позволяет описать всю инфраструктуру для работы продукта целиком.
Про Terraform можно прочитать статью моего коллеги из DINS.


Собственно, давайте разберем, что за ресурсы нам необходимо объявить в SAM-шаблоне.

AWS: Serverless: Function
Основные характеристики уже заполнены за нас, если мы использовали SAM init для создания проекта. Но вот как они выглядят в моем примере:

CodeUri: contact-us-function
Handler: com.gralll.sam.App::handleRequest
Runtime: java8
MemorySize: 256


Тут думаю все достаточно понятно.
CodeUri — это директория с нашей лямбдой
Handler — полный путь до метода
Runtime — то, на чем написана лямбда
MemorySize — говорит само за себя

Кстати о памяти: в лямбде доступно до 3GB оперативной памяти, что соответствует ресурсу в 2 CPU. То есть вы не можете регулировать CPU отдельно, только повышая/понижая количество памяти.

Следующий блок нужен для выбора способа деплоймента.

AutoPublishAlias: live
DeploymentPreference:
  Type: Canary10Percent10Minutes


AutoPublishAlias — позволяет на каждую новую задеплоенную версию добавить алиас. Это нужно для того, чтобы реализовать canary-деплоймент.
Canary10Percent10Minutes — тип деплоймента, который позволит держать одновременно две версии лямбды: старую и новую, но на новую перенаправить только 10% трафика. Если за десять минут никаких проблем не возникнет, остальной трафик также будет перенаправлен на новую версию.
Подробнее об использовании расширенных возможностей можно почитать на странице SAM.

Далее идут переменные окружения, которые будут использоваться из кода. После этого еще один блок для лямбды:

Events:
  ContactUs:
    Type: Api
    Properties:
      Path: /contact
      Method: post


В нем мы должны описать триггеры для вызова лямбды. В нашем случае это будут API Gateway-запросы.
Это довольно упрощенная форма описания API, но этого достаточно чтобы новый созданный API Gateway перенаправлял все запросы типа POST /contact на нашу лямбду.

Конечно, нам нужно описать секьюрные аспекты. Из коробки созданная лямбда не будет иметь доступа ни к базе данных, ни к email-сервису. Так что нам нужно явно прописать, что будет разрешено. Существует несколько способов дать доступ внутри AWS. Мы будем использовать Resource-Based Policies:

Policies:
  - AWSLambdaExecute
  - Version: '2012-10-17'
    Statement:
      - Effect: Allow
        Action:
          - ses:SendEmail
          - ses:SendRawEmail
        Resource: 'arn:aws:ses:eu-west-1:548476639829:identity/aleksandrgruzdev11@gmail.com'
      - Effect: Allow
        Action:
          - dynamodb:List*
        Resource: '*'
      - Effect: Allow
        Action:
          - dynamodb:Get*
          - dynamodb:PutItem
          - dynamodb:DescribeTable
        Resource: 'arn:aws:dynamodb:*:*:table/ContactUsTable'


Обратите внимание, что для таблицы базы данных мы указали конкретное имя, следовательно, нам нужно будет создать таблицу с таким же именем.
Что касается SES: вы видите мой адрес электронной почты. В вашем случае это должен быть ваш собственный подтвержденный адрес. Как это сделать, смотрите тут.
Сразу после этого вы сможете найти Identity ARN этого ресурса, кликнув по созданному адресу, и заменить им емейл в примере выше.
С лямбдой вроде разобрались. Теперь перейдем к БД.

AWS: Serverless: SimpleTable
Для наших задач мы создадим лишь одну таблицу ContactUsTable:

ContactUsTable:
  Type: AWS::Serverless::SimpleTable
  Properties:
    PrimaryKey:
      Name: Id
      Type: String
    TableName: ContactUsTable
    ProvisionedThroughput:
      ReadCapacityUnits: 2
      WriteCapacityUnits: 2


Из обязательных полей — лишь Id, а также укажем ReadCapacityUnits и WriteCapacityUnits. Не будем подробно останавливаться на том какие выбрать значения, так как это тоже довольно обширная тема. Можно почитать тут. Для тестового приложения достаточно и малых значений порядка 1–2.

Globals
В этот блок можно вынести общие параметры если, к примеру, вы объявляете несколько ресурсов типа Function или API.

Globals:
  Function:
    Timeout: 15
  Api:
    Cors:
      AllowOrigin: "'*'"
      AllowHeaders: "'Content-Type,X-WARM-UP,X-Amz-Date,Authorization,X-Api-Key'"


Я использовал его, чтобы установить таймаут для функции и некоторые Cors-настройки — чтобы позже вызывать API Gateway с моей статической страницы с ContactUs формой.

Outputs
Этот блок позволяет динамически определить некоторые переменные в глобальном контексте AWS CloudFormation.

Outputs:
  ContactUsApi:
    Description: "API Gateway endpoint URL for Prod stage for ContactUs function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/contact/"
  ContactUsFunction:
    Description: "ContactUs Lambda Function ARN"
    Value: !GetAtt ContactUsFunction.Arn
  ContactUsFunctionIamRole:
    Description: "Implicit IAM Role created for ContactUs function"
    Value: !GetAtt ContactUsFunctionRole.Arn


К примеру, мы объявили ContactUsApi-переменную, которой будет задано значение, как публичный адрес нашего созданного API-эндпоинта.
Так как мы используем ${ServerlessRestApi}, AWS сам подставит в строку уникальный идентификатор нашего нового API Gateway. В итоге любое приложение, имеющее доступ к CloudFormation, сможет достать этот адрес — тем самым вы можете не хардкодить URL ваших сервисов. Ну и еще плюс в том, что видеть список аутпутов очень удобно — некая мета информация о вашем стеке.
С полным списком функций и какие параметры вы можете использовать можно ознакомиться тут.

Parameters
В дополнение ко всему вышеописанному, вы можете добавить блок Parameters. Эти параметры помогут сделать шаблон более универсальным. В любом другом блоке шаблона вы сможете делать ссылки на эти параметры, тем самым не хардкодить некоторые значения. К примеру, в моем шаблоне это могут быть емейлы, SES ARN, количество памяти и т.д.

Вот и весь шаблон. Никто не запрещает добавить еще пару ресурсов рядом, например, S3-бакет, любой другой CloudFormation-ресурс, либо вообще кастомный ресурс.

6. Приступаем к деплойменту


Для того, чтобы собрать наш проект, будем использовать не Gradle, а SAM.

Сборка
F:\aws\projects\contact-us-sam-app>sam build
Building resource 'ContactUsFunction'
Running JavaGradleWorkflow:GradleBuild
Running JavaGradleWorkflow:CopyArtifacts

Build Succeeded

Built Artifacts  : .aws-sam\build
Built Template   : .aws-sam\build\template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy –guided


Запуск sam build команды из рута проекта автоматически соберет в папку .aws-sam все необходимые файлы: классы, зависимости, шаблон SAM.
Далее необходимо создать S3-бакет, куда SAM впоследствии загрузит все собранные артефакты.
Это можно сделать либо через браузерную AWS-консоль, либо командой

aws s3 mb s3://bucket-name


Заметка: все бакеты создаются в глобальном контексте, а он шарится между всеми аккаунтами. Так что вы не сможете создать бакет, если кто-то уже его создал в своем аккаунте.

Когда бакет готов, выполняем команду:

sam package --output-template-file packaged.yaml --s3-bucket


Результат команды package
F:\aws\projects\contact-us-sam-app>sam package --output-template-file packaged.yaml --s3-bucket contact-us-sam-app
Uploading to ea0c122c06a50d9676fbf9000a80a3bf  9212768 / 9212768.0  (100.00%)

Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
sam deploy --template-file F:\aws\projects\contact-us-sam-app\packaged.yaml --stack-name 

Я указал свой бакет contact-us-sam-app, и SAM загрузил все ресурсы в указанное место. Далее SAM уже подсказывает вам команду, чтобы создать стек с ресурсами, и тем самым задеплоить ваше решение. Выполняем команду, немного доработав:

sam deploy --template-file packaged.yaml --region eu-west-1 --capabilities CAPABILITY_IAM --stack-name contact-us-sam-app


Как вы можете видеть, я добавил --capabilities CAPABILITY_IAM. Это позволит CloudFormation создавать IAM-ресурсы. Иначе при создании вы получите ошибку InsufficientCapabilities.
Далее представлен лог работы этой команды (Картинка кликабельна). Такие подробности, как состояние каждого ресурса и значения аутпутов, стали доступны только в одной из последних версий SAM CLI.
nv7p4zreqati1wd8c8vy3ubuzcm.png

7. Проверяем статус CloudFormation


Можем ждать завершения деплоймента в консоли, пока не появится сообщение о том, что стек был задеплоен (логи команды deploy в предыдущем абзаце):

Successfully created/updated stack - contact-us-sam-app in eu-west-1


Но в нашем случае вы увидите это сообщение только через десять минут из-за режима canary deployment. Поэтому проще открыть браузерную консоль и наблюдать за стеком там.
97kik8apukwxsvqwyvsd_wtmaqo.png
Через некоторое время статус сменится на CREATE_COMPLETE, что и будет означать успешное завершение.
Во вкладке Events можно видеть статус всех ресурсов. Если ваш стек зафейлится, именно тут вы сможете найти подробные сообщения об ошибках.
К примеру, такое: UPDATE_FAILED — если вы неверно сконфигурируете API Gateway в шаблоне.
cemhaqtl6uvdmlvnr0bsvrbokok.png

Во вкладке Resources можно найти все созданные ресурсы. Не удивляйтесь их количеству. Хотя мы объявили в SAM-шаблоне только функцию и таблицу базы данных, CloudFormation создал за нас много других ресурсов. Если посмотреть на их Type, можно понять, какому сервису они принадлежат.
Для Api Gateway неявно было создано:

  • ServerlessRestApi
  • ServerlessRestApiDeployment
  • ServerlessRestApiProdStage


Также и для лямбды было создано несколько дополнительных объектов.
Теперь откроем Outputs и найдем URL нашего API. Скопируйте ее, она нам скоро пригодится.

8. Форма HTML ContactUs


Как вы помните, я решил делать ContactUs-форму, и сейчас нам нужно как-то сделать ее доступной не только на локальной машине.

Конфигурирование
Что касается самой формы, то для примера я решил взять простейшую HTML-форму и добавил вызовы API Gateway через ajax.
pse-4hta3mojfuamij0dhj3bmyg.png
Кроме самой формы, я добавил пару кнопок для дебага и упрощения заполнения:
Set default — подставляет, как ожидаемо это звучит, дефолтные параметры заданные внутри HTML.

$("#url-input").val("https://4ykskscuq0.execute-api.eu-west-1.amazonaws.com/Prod/contact");
$("#name-input").val("Mike");
$("#email-input").val("Mike@somemail.com");
$("#phone-input").val("+79999999999");
$("#description-input").val("How much does it cost?");


Если вы собираетесь использовать этот функционал, поменяйте url-input на путь до вашего API Gateway, который вы скопировали из Outputs.

Хостинг

  • Создаем новый S3-бакет.
  • Загружаем HTML файл в бакет выбирая опцию сделать файл публичным.
  • Заходим в бакет куда мы загрузили файл.
  • Переходим в Properties, далее включаем Static website hosting и видим ваш новый публично доступный Endpoint.

9. Проверка работоспособности


Стандартный запрос
Теперь вы можете перейти по ссылке на вашу форму, нажать Set default и Send.
Таким образом вы сделаете запрос который пройдет через API Gateway до AWS Lambda.

Если вы настроили все правильно через некоторое время вы получите сообщение наподобие:

Successful: Message 0102016f28b06243-ae897a1e-b805–406b-9987–019f21547682–000000 has been sent successfully.


Это значит, что сообщение было успешно доставлено. Проверяйте ваш почтовый ящик, который вы указали в SAM-темплейте. Если вы не меняли шаблон то письмо будет в таком формате:
0zwysv5ytv3uzn9fb9az7bsa6lg.png
Также можете открыть DynamoDB и убедиться, что новая запись появилась.

Особенности холодного старта
Я думаю, вы заметили, что сообщение об успешной отправке пришло через довольно длительное время. Это связано с тем, что сервис AWS Lambda, получив запрос на обработку, начал поднимать инстанс вашего Java-приложения, а это включает поднятие контейнера с операционной системой и JRE, загрузку classpath, инициализацию всех переменных и уже только после этого — старт метода handleRequest (). Это называется холодным стартом.

Попробуйте еще раз заполнить и отправить форму. На этот раз ответ пришел почти моментально, так ведь? Если между первым и вторым запросом прошло более 20–30 минут, то результат может отличаться.
С чем это связано? А с тем, что AWS Lambda кеширует уже отработавшие контейнеры с лямбдами для их повторного использования. Таким образом снижается длительность инициализации всего контекста для запуска метода.
Нет четкого соотношения, на какое время лямбды кешируются в зависимости от чего бы то ни было, но некоторые люди опытным путем устанавливали, что это напрямую зависит от выбранного значения оперативной памяти. То есть лямбда с 128MB памяти будет доступна дольше, чем с 3GB. Возможно, есть и другие параметры, например, средняя нагрузка на регион, в котором ваши лямбды выполняются, но это неточно.
Поэтому экспериментируйте сами и планируйте время кеширования, если вы используете синхронные запросы.

Прогрев
Как дополнительную опцию, вы можете использовать прогрев лямбды. В коде я добавил проверку X-WAMP-UP-заголовка. В случае наличия такового, лямбда просто возвращает ответ, не выполняя никакой бизнес-логики, но контейнер будет готов для последующих вызовов.
Вы можете самостоятельно вызывать вашу лямбду, например, по крон-таймеру, используя CloudWatch. Это поможет в случае, когда вы не желаете, чтобы ваши клиенты попадали на Cold Start.
В HTML-форме вы можете найти кнопку WarmUp mode, которая добавляет этот специальный заголовок в запрос. Вы можете проверить, что ни отправка письма, ни запись в базу не происходит, но ответ от лямбды приходит, и последующий вызов уже настоящего запроса займет не так много времени.


В ходе статьи мы прошли все основные этапы от дизайна приложения до выхода его в так называемый продакшн.
Я надеюсь, что те люди, кто уже слышал про Serverless и AWS Lambda, но не имевшие практического опыта, смогут применить этот гайд и смогут прочувствовать, что это дает значительные преимущества в скорости разворачивания некоторых программных решений и не только в этом.

Преимущества
Для себя я выделил самые ценные преимущества:

  • Это совершенно бесплатно в рамках Free Tier и я могу поиграться с сервисами, с которыми очень хотелось поработать, но на основном проекте не было возможности.
  • От имплементации метода лямбды до работающего прототипа в облаке может пройти 1–2 дня. То есть очень быстрый порог вхождения в разработку/деплоймент.
  • Присутствует браузерная консоль, что позволяет на первых парах деплоить приложения интуитивно клацая кнопки, а не с самого начала погружаться в километровые скрипты.
  • Увеличивается аудитория Serverless и тем самым улучшается поддержка разработки. Появляются новые тулы, существующие — несомненно совершенствуются. Тот же SAM фреймворк быстро прогрессирует и уже взят под крыло самим AWS. AWS релизит апдейты огромными темпами, и то что требовало недели кастомизации пол года назад, сейчас занимает минуты.
  • Огромные количество шаблонов готовых решений в Lambda репозитории, которые можно использовать для тренировок.
  • Наличие уже готовой Serverless инфраструктуры для сбора логов, метрик, аналитики. Нет нужды тратить драгоценное время на установку и поддержку ELK/Promethes/Grafana если нет принципиальных требований к тулам.
  • Встроенные механизмы обеспечения безопасности, позволяющие ограничить доступ как к вызову лямбды так и к вызову всего API. Используя в комбинации как самые простые принципы такие как API Key, так и IAM политики, можно создать достаточно быстро правила доступа к сервису и не бояться за лишние траты в случае если один из механизмов был скомпрометирован.
  • Ну и самое основное наверно, это — Serverless. Нет необходимости думать о том, какой инстанс EC2 нужно оплатить, как настроить к нему доступы, как настроить алерты в случае падения приложения, настраивать авто-масштабирование и т.д.

Недостатки
К сожалению, это не значит, что теперь можно взять это решение и использовать как часть энтерпрайз системы вместо любимых микросервисов в кубере. Очень часто нужно копнуть глубже, чтобы определить более подходящее в конкретном случае решение.
И у меня также имеются замечания, на которые стоит обратить внимание при выборе Lambda:

  • Холодный старт. Особенно значим при использовании синхронных запросов, и таких языков таких как Java или C#. Проблема умеренно решается прогревом, но лучше подумать над таким решением заранее и сопоставить стоимость и возможные выгоды
  • Ограничения по памяти и процессору. Если ваше приложение может перевалить за 3GB оперативной памяти, либо нуждается в 2 и более ядрах, стоит подумать над тем, чтобы перенести это в какой-нибудь Fargate, либо разделить нагрузку на несколько вызовов лямбд, но тем самым усложнив архитектуру.
  • Когда у вас на данный момент очень низкая нагрузка на API, но существует вероятность повышения ее в разы, то я бы прикинул по стоимости во что это может обойтись.
  • Если у вас у команде/проекте никто кроме 1–2 человек не работал с Serverless, будет достаточно проблематично построить всю инфраструктуру и особенно сложно спроектировать архитектуру и заставить людей ее поддерживать.
  • Если у вас уже готова инфраструктура для разработки микросервисов и CI/CD в кубере, будет опять же проблематично аргументировать (особенно менеджерам) необходимость поддержки еще одного процесса CI/CD.
  • Ну и куда уж без тестирования. А протестировать лямбду на пропускную способность довольно трудно, ведь это все же отличается от обычного perfarmance тестирования, и нужно учесть много факторов.

Использование
В общем и целом, использование AWS Lambda и других Serverless сервисов очень хорошо ложится на принципы async и event-driven development. И именно используя асинхронную обработку можно добиться оптимальных результатов как по производительности, так и по стоимости. Вот список решений в которых Serveress решения будут играть значимую роль:
i6ovy6ha2pszsiui5jvyewbf-dk.png


Так как статья и так получилась довольно обширная, и не хочется раздувать ее еще больше, я скорее всего подготовлю продолжение, в котором покажу как вести разработку эффективнее, используя все возможности AWS консоли, SAM фреймворка и даже IntelljIDEA. Ну и так как я опустил часть про тестирование, то и этот аспект разработки постараюсь описать более подробно. Также если у вас есть пожелания, что добавить в следующую статью, или вопросы, пишите смело в комментарии, либо в личные сообщения.

Некоторые важные и полезные ссылки из статьи:

© Habrahabr.ru