Использование AWS Lambda с API Gateway

Использование AWS Lambda с API-шлюзом

1. обзор

AWS Lambda - это сервис бессерверных вычислений, предоставляемый Amazon Web Services.

В двух предыдущих статьях мы обсуждалиhow to create an AWS Lambda function using Java, а такжеhow to access DynamoDB from a Lambda function.

В этом руководстве мы обсудимhow to publish a Lambda function as a REST endpoint, using AWS Gateway.

Мы подробно рассмотрим следующие темы:

  • Основные понятия и условия API Gateway

  • Интеграция функций Lambda с API Gateway с использованием интеграции Lambda Proxy

  • Создание API, его структура и способ сопоставления ресурсов API с функциями Lambda.

  • Развертывание и тестирование API

2. Основы и термины

API Gateway - этоfully managed service that enables developers to create, publish, maintain, monitor, and secure APIs at any scale.

Мы можем реализовать согласованный и масштабируемый программный интерфейс на основе HTTP (также называемый службами RESTful)to access backend services like Lambda functions, further AWS services (e.g., EC2, S3, DynamoDB), and any HTTP endpoints.

Особенности включают, но не ограничиваются:

  • Управление движением

  • Авторизация и контроль доступа

  • мониторинг

  • Управление версиями API

  • Регулирование запросов для предотвращения атак

Как и AWS Lambda, API-шлюз автоматически масштабируется и оплачивается за вызов API.

Подробную информацию можно найти вofficial documentation.

2.1. термины

API Gateway - это сервис AWS, который поддерживает создание, развертывание и управление интерфейсом прикладного программирования RESTful для предоставления серверных конечных точек HTTP, функций AWS Lambda и других сервисов AWS.

API Gateway API - это набор ресурсов и методов, которые можно интегрировать с функциями Lambda, другими сервисами AWS или конечными точками HTTP в серверной части. API состоит из ресурсов, которые формируют структуру API. Каждый ресурс API может предоставлять один или несколько методов API, которые должны иметь уникальные HTTP-глаголы.

Чтобы опубликовать API, мы должны создатьAPI deployment и связать его с так называемымstage. Этап похож на моментальный снимок API. Если мы повторно развернем API, мы можем либо обновить существующий этап, либо создать новый. Таким образом, одновременно возможны разные версии API, например этапdev, этапtest и даже несколько производственных версий, напримерv1,v2 и т. д.

Lambda Proxy integration - это упрощенная конфигурация для интеграции между лямбда-функциями и API Gateway.

Шлюз API отправляет весь запрос в качестве входных данных бэкэнд-функции Lambda. Что касается отклика, API Gateway преобразует вывод лямбда-функции обратно в ответ HTTP внешнего интерфейса.

3. зависимости

Нам потребуются те же зависимости, что и в статьеAWS Lambda Using DynamoDB With Java.

Вдобавок нам понадобится библиотекаJSON Simple:


    com.googlecode.json-simple
    json-simple
    1.1.1

4. Разработка и развертывание лямбда-функций

В этом разделе мы разработаем и создадим наши Lambda-функции на Java, мы развернем их с помощью Консоли AWS и проведем быстрый тест.

Поскольку мы хотим продемонстрировать основные возможности интеграции API Gateway с Lambda, мы создадим две функции:

  • Function 1: получает полезную нагрузку от API, используя метод PUT

  • Function 2: демонстрирует, как использовать параметр пути HTTP или параметр запроса HTTP, поступающий из API.

Что касается реализации, мы создадим один классRequestHandler, у которого есть два метода - по одному для каждой функции.

4.1. модель

Прежде чем мы реализуем фактический обработчик запросов, давайте взглянем на нашу модель данных:

public class Person {

    private int id;
    private String name;

    public Person(String json) {
        Gson gson = new Gson();
        Person request = gson.fromJson(json, Person.class);
        this.id = request.getId();
        this.name = request.getName();
    }

    public String toString() {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        return gson.toJson(this);
    }

    // getters and setters
}

Наша модель состоит из одного простого классаPerson, который имеет два свойства. Единственная примечательная часть - это конструкторPerson(String), который принимает строку JSON.

4.2. Реализация класса RequestHandler

Как и в статьеAWS Lambda With Java, мы создадим реализацию интерфейсаRequestStreamHandler:

public class APIDemoHandler implements RequestStreamHandler {

    private static final String DYNAMODB_TABLE_NAME = System.getenv("TABLE_NAME");

    @Override
    public void handleRequest(
      InputStream inputStream, OutputStream outputStream, Context context)
      throws IOException {

        // implementation
    }

    public void handleGetByParam(
      InputStream inputStream, OutputStream outputStream, Context context)
      throws IOException {

        // implementation
    }
}

Как мы видим, интерфейсRequestStreamHander определяет только один метод,handeRequest(). В любом случае, мы можем определить дополнительные функции в том же классе, как мы это сделали здесь. Другой вариант - создать по одной реализацииRequestStreamHander для каждой функции.

В нашем конкретном случае мы выбрали первый для простоты. Тем не менее, выбор должен быть сделан в каждом конкретном случае, принимая во внимание такие факторы, как производительность и ремонтопригодность кода.

Мы также читаем имя нашей таблицы DynamoDB из переменной окруженияTABLE_NAME . Мы определим эту переменную позже во время развертывания.

4.3. Реализация функции 1

В нашей первой функции мы хотим продемонстрироватьhow to get a payload (like from a PUT or POST request) from the API Gateway:

public void handleRequest(
  InputStream inputStream,
  OutputStream outputStream,
  Context context)
  throws IOException {

    JSONParser parser = new JSONParser();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    JSONObject responseJson = new JSONObject();

    AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
    DynamoDB dynamoDb = new DynamoDB(client);

    try {
        JSONObject event = (JSONObject) parser.parse(reader);

        if (event.get("body") != null) {
            Person person = new Person((String) event.get("body"));

            dynamoDb.getTable(DYNAMODB_TABLE_NAME)
              .putItem(new PutItemSpec().withItem(new Item().withNumber("id", person.getId())
                .withString("name", person.getName())));
        }

        JSONObject responseBody = new JSONObject();
        responseBody.put("message", "New item created");

        JSONObject headerJson = new JSONObject();
        headerJson.put("x-custom-header", "my custom header value");

        responseJson.put("statusCode", 200);
        responseJson.put("headers", headerJson);
        responseJson.put("body", responseBody.toString());

    } catch (ParseException pex) {
        responseJson.put("statusCode", 400);
        responseJson.put("exception", pex);
    }

    OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
    writer.write(responseJson.toString());
    writer.close();
}

Как уже говорилось ранее, позже мы настроим API для использования интеграции с Lambda-прокси. Мы ожидаем, что API-шлюз передаст полный запрос функции Lambda в параметреInputStream.

Все, что нам нужно сделать, это выбрать соответствующие атрибуты из содержащейся в нем структуры JSON.

Как мы видим, метод в основном состоит из трех этапов:

  1. Получение объектаbody из нашего входного потока и создание объектаPerson из этого

  2. Сохранение этого объектаPerson в таблице DynamoDB

  3. Создание объекта JSON, который может содержать несколько атрибутов, напримерbody для ответа, настраиваемые заголовки, а также код состояния HTTP.

Здесь стоит упомянуть один момент: API Gateway ожидает, чтоbody будетString (как для запроса, так и для ответа).

Поскольку мы ожидаем получитьString какbody от шлюза API, мы приводимbody кString и инициализируем наш объектPerson:

Person person = new Person((String) event.get("body"));

API Gateway также ожидает, что ответbody будетString:

responseJson.put("body", responseBody.toString());

Эта тема явно не упоминается в официальной документации. Однако, если мы внимательно рассмотрим, то увидим, что атрибут body - этоString в обоих фрагментахfor the request, а такжеfor the response.

Преимущество должно быть очевидным: даже если JSON является форматом между API Gateway и функцией Lambda, фактическое тело может содержать простой текст, JSON, XML или любой другой. В этом случае функция Lambda отвечает за правильную обработку формата.

Мы увидим, как выглядят тела запроса и ответа, позже, когда мы протестируем наши функции в Консоли AWS.

То же самое относится и к следующим двум функциям.

4.4. Implementation of  Функция 2

На втором этапе мы хотим продемонстрироватьhow to use a path parameter or a query string parameter для получения элементаPerson из базы данных с использованием его идентификатора:

public void handleGetByParam(
  InputStream inputStream, OutputStream outputStream, Context context)
  throws IOException {

    JSONParser parser = new JSONParser();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    JSONObject responseJson = new JSONObject();

    AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
    DynamoDB dynamoDb = new DynamoDB(client);

    Item result = null;
    try {
        JSONObject event = (JSONObject) parser.parse(reader);
        JSONObject responseBody = new JSONObject();

        if (event.get("pathParameters") != null) {
            JSONObject pps = (JSONObject) event.get("pathParameters");
            if (pps.get("id") != null) {
                int id = Integer.parseInt((String) pps.get("id"));
                result = dynamoDb.getTable(DYNAMODB_TABLE_NAME).getItem("id", id);
            }
        } else if (event.get("queryStringParameters") != null) {
            JSONObject qps = (JSONObject) event.get("queryStringParameters");
            if (qps.get("id") != null) {

                int id = Integer.parseInt((String) qps.get("id"));
                result = dynamoDb.getTable(DYNAMODB_TABLE_NAME)
                  .getItem("id", id);
            }
        }
        if (result != null) {
            Person person = new Person(result.toJSON());
            responseBody.put("Person", person);
            responseJson.put("statusCode", 200);
        } else {
            responseBody.put("message", "No item found");
            responseJson.put("statusCode", 404);
        }

        JSONObject headerJson = new JSONObject();
        headerJson.put("x-custom-header", "my custom header value");

        responseJson.put("headers", headerJson);
        responseJson.put("body", responseBody.toString());

    } catch (ParseException pex) {
        responseJson.put("statusCode", 400);
        responseJson.put("exception", pex);
    }

    OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
    writer.write(responseJson.toString());
    writer.close();
}

Опять же, три шага актуальны:

  1. Мы проверяем, присутствует ли массивpathParameters илиqueryStringParameters с атрибутомid.

  2. Еслиtrue, мы используем принадлежащее значение для запроса элементаPerson с этим идентификатором из базы данных.

  3. Мы добавляем JSON-представление полученного элемента в ответ.

Официальная документация предоставляет более подробное объяснениеinput format иoutput format для интеграции с прокси.

4.5. Строительный кодекс

Опять же, мы можем просто построить наш код, используя Maven:

mvn clean package shade:shade

Файл JAR будет создан в папкеtarget.

4.6. Создание таблицы DynamoDB

Мы можем создать таблицу, как описано вAWS Lambda Using DynamoDB With Java.

Давайте выберемPerson в качестве имени таблицы,id в качестве имени первичного ключа иNumber в качестве типа первичного ключа.

4.7. Развертывание кода через Консоль AWS

После построения нашего кода и создания таблицы, теперь мы можем создать функции и загрузить код.

Это можно сделать, повторив шаги 1–5 из статьиAWS Lambda with Java по одному разу для каждого из двух наших методов.

Давайте использовать следующие имена функций:

  • StorePersonFunction для методаhandleRequest (функция 1)

  • GetPersonByHTTPParamFunction  для методаhandleGetByParam (функция 2)

Мы также должны определить переменную окруженияTABLE_NAME со значением“Person”.

4.8. Тестирование функций

Прежде чем перейти к фактической части API Gateway, мы можем запустить быстрый тест в консоли AWS, просто чтобы убедиться, что наши функции Lambda работают правильно и могут обрабатывать формат интеграции Proxy.

Тестирование функции Lambda из консоли AWS работает, как описано в статьеAWS Lambda with Java.

Однакоwhen we create a test event, we have to consider the special Proxy Integration format, которого ожидают наши функции. Мы можем либо использовать шаблонAPI Gateway AWS Proxy и настроить его для наших нужд, либо скопировать и вставить следующие события:

ДляStorePersonFunction мы должны использовать это:

{
    "body": "{\"id\": 1, \"name\": \"John Doe\"}"
}

Как обсуждалось ранее,body должен иметь типString, даже если он содержит структуру JSON. Причина в том, что API-шлюз будет отправлять свои запросы в том же формате.

Следующий ответ должен быть возвращен:

{
    "isBase64Encoded": false,
    "headers": {
        "x-custom-header": "my custom header value"
    },
    "body": "{\"message\":\"New item created\"}",
    "statusCode": 200
}

Здесь мы видим, чтоbody нашего ответа - этоString, хотя он содержит структуру JSON.

Давайте посмотрим на ввод дляGetPersonByHTTPParamFunction.

Для тестирования функциональности параметра пути входные данные должны выглядеть следующим образом:

{
    "pathParameters": {
        "id": "1"
    }
}

И вход для отправки параметра строки запроса будет:

{
    "queryStringParameters": {
        "id": "1"
    }
}

В ответ мы должны получить следующее для обоих методов:

{
  "headers": {
    "x-custom-header": "my custom header value"
  },
  "body": "{\"Person\":{\n  \"id\": 88,\n  \"name\": \"John Doe\"\n}}",
  "statusCode": 200
}

Опять же,body - этоString.

5. Создание и тестирование API

После того, как мы создали и развернули лямбда-функции в предыдущем разделе,we can now create the actual API using the AWS Console.

Давайте посмотрим на основной рабочий процесс:

  1. Создайте API в нашей учетной записи AWS.

  2. Добавьте ресурс в иерархию ресурсов API.

  3. Создайте один или несколько методов для ресурса.

  4. Настройте интеграцию между методом и принадлежащей лямбда-функцией.

В следующих разделах мы повторим шаги 2–4 для каждой из двух функций.

5.1. Создание API

Для создания API нам потребуется:

  1. Войдите в консоль API Gateway наhttps://console.aws.amazon.com/apigateway.

  2. Нажмите «Начать» и затем выберите «Новый API»

  3. Введите имя нашего API (TestAPI) и подтвердите, нажав «Создать API».

Создав API, мы теперь можем создать структуру API и связать ее с нашими функциями Lambda.

5.2. Структура API для функции 1

Для нашегоStorePersonFunction необходимы следующие шаги:

  1. Выберите родительский элемент ресурса в дереве «Ресурсы», а затем выберите «Создать ресурс» в раскрывающемся меню «Действия». Затем мы должны сделать следующее на панели «New Child Resource»:

    • Введите «Персоны» в качестве имени в поле ввода «Имя ресурса».

    • Оставьте значение по умолчанию в поле ввода «Resource Path»

    • Выберите «Создать ресурс»

  2. Выберите только что созданный ресурс, выберите «Создать метод» в раскрывающемся меню «Действия» и выполните следующие действия:

    • Выберите PUT из раскрывающегося списка метода HTTP, а затем выберите значок галочки, чтобы сохранить выбор

    • Оставьте «Лямбда-функцию» в качестве типа интеграции и выберите «Использовать Лямбда-прокси»

    • Выберите регион из «Lambda Region», в котором мы ранее использовали функции Lambda.

    • Введите“StorePersonFunction” в «Лямбда-функция»

  3. Выберите «Сохранить» и подтвердите, нажав «ОК», когда появится запрос «Добавить разрешение к лямбда-функции».

5.3. Структура API для функции 2 - параметры пути

Шаги для получения параметров пути аналогичны:

  1. Выберите элемент ресурса/persons в дереве «Ресурсы», а затем выберите «Создать ресурс» в раскрывающемся меню «Действия». Затем мы должны сделать следующее на панели New Child Resource:

    • Введите“Person” в качестве имени в текстовом поле ввода «Имя ресурса».

    • Измените текстовое поле ввода «Путь к ресурсам» на“{id}”

    • Выберите «Создать ресурс»

  2. Выберите только что созданный ресурс, выберите «Метод создания» в раскрывающемся меню «Действия» и выполните следующие действия:

    • Выберите GET из выпадающего списка метода HTTP, а затем выберите значок галочки, чтобы сохранить выбор

    • Оставьте «Лямбда-функцию» в качестве типа интеграции и выберите «Использовать Лямбда-прокси»

    • Выберите регион из «Lambda Region», в котором мы ранее использовали функции Lambda.

    • Введите“GetPersonByHTTPParamFunction” в «Лямбда-функция»

  3. Выберите «Сохранить» и подтвердите, нажав «ОК», когда появится запрос «Добавить разрешение к лямбда-функции».

Примечание: здесь важно установить для параметра «Путь к ресурсам» значение“{id}”, так как нашGetPersonByPathParamFunction считает, что этот параметр будет называться именно так.

5.4. Структура API для функции 2 - параметры строки запроса

Шаги для получения параметров строки запроса немного отличаются, напримерwe don’t have to create a resource, but instead have to create a query parameter for the id parameter:

  1. Выберите элемент ресурса/persons в дереве «Ресурсы», выберите «Создать метод» в раскрывающемся меню «Действия» и выполните следующие действия:

    • Выберите GET из выпадающего списка метода HTTP, а затем нажмите значок галочки, чтобы сохранить выбор

    • Оставьте «Лямбда-функцию» в качестве типа интеграции и выберите «Использовать Лямбда-прокси»

    • Выберите регион из «Lambda Region», в котором мы ранее использовали функции Lambda.

    • Введите“GetPersonByHTTPParamFunction” в «Лямбда-функция».

  2. Выберите «Сохранить» и подтвердите, нажав «ОК», когда появится запрос «Добавить разрешение к лямбда-функции».

  3. Выберите «Запрос метода» справа и выполните следующие шаги:

    • Разверните список параметров строки запроса URL

    • Нажмите «Добавить строку запроса»

    • Введите“id” в поле имени и выберите значок галочки, чтобы сохранить

    • Установите флажок «Обязательно»

    • Нажмите на значок пера рядом с «Запросить валидатор» в верхней части панели, выберите «Проверить параметры строки запроса и заголовки» и выберите значок галочки

Примечание. Важно установить для параметра «Строка запроса» значение“id”, так как нашGetPersonByHTTPParamFunction считает, что этот параметр будет называться именно так.

5.5. Тестирование API

Наш API готов, но еще не опубликован. Before we publish it, we want to run a quick test from the Console first.

Для этого мы можем выбрать соответствующий метод для тестирования в дереве «Ресурсы» и нажать кнопку «Тест». На следующем экране мы можем ввести наш ввод, как если бы мы отправляли его клиенту через HTTP.

ДляStorePersonFunction мы должны ввести следующую структуру в поле «Request Body»:

{
    "id": 2,
    "name": "Jane Doe"
}

ДляGetPersonByHTTPParamFunction с параметрами пути мы должны ввести2 в качестве значения в поле «{id}» в разделе «Путь».

Для параметров строки запросаGetPersonByHTTPParamFunction with мы должны ввестиid=2 в качестве значения в поле «{person}» в разделе «Query Strings».

5.6. Развертывание API

До сих пор наш API не был общедоступным и поэтому был доступен только из Консоли AWS.

Как обсуждалось ранее,when we deploy an API, we have to associate it with a stage, which is like a snapshot in time of the API. If we redeploy an API, we can either update an existing stage or create a new one.

Посмотрим, как будет выглядеть схема URL для нашего API:

https://{restapi-id}.execute-api.{region}.amazonaws.com/{stageName}

Для развертывания необходимы следующие шаги:

  1. Выберите конкретный API на панели навигации «APIs»

  2. Выберите «Действия» в области навигации «Ресурсы» и выберите «Развернуть API» в раскрывающемся меню «Действия».

  3. Выберите «[Новый этап]» в раскрывающемся списке «Этап развертывания», введите“test” в «Имя этапа» и, при желании, предоставьте описание этапа и развертывания.

  4. Запустите развертывание, выбрав «Развернуть»

После последнего шага консоль предоставит корневой URL-адрес API, напримерhttps://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test.

5.7. Вызов конечной точки

Поскольку API теперь общедоступен,we can call it using any HTTP client we want.

СcURL вызовы будут выглядеть следующим образом.

StorePersonFunction:

curl -X PUT 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons' \
  -H 'content-type: application/json' \
  -d '{"id": 3, "name": "Richard Roe"}'

GetPersonByHTTPParamFunction  для параметров пути:

curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons/3' \
  -H 'content-type: application/json'

GetPersonByHTTPParamFunction для параметров строки запроса:

curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons?id=3' \
  -H 'content-type: application/json'

6. Заключение

В этой статье мы рассмотрели, как сделать функции AWS Lambda доступными как конечные точки REST, используя AWS API Gateway.

Мы изучили основные концепции и терминологию API Gateway и узнали, как интегрировать функции Lambda с помощью интеграции Lambda Proxy.

Наконец, мы увидели, как создавать, развертывать и тестировать API.

Как обычно, весь код для этой статьи доступен черезon GitHub.