Използване на AWS Lambda с API шлюз

1. Общ преглед

AWS Lambda е изчислителна услуга без сървър, предоставяна от Amazon Web Services.

В две предишни статии обсъдихме как да създадем AWS Lambda функция с помощта на Java, както и как да получим достъп до DynamoDB от Lambda функция.

В този урок ще обсъдим как да публикуваме Lambda функция като REST крайна точка, използвайки AWS Gateway .

Ще разгледаме подробно следните теми:

  • Основни понятия и термини на API Gateway
  • Интеграция на Lambda функции с API Gateway с помощта на Lambda Proxy интеграция
  • Създаване на API, неговата структура и как да се картографират ресурсите на API върху функциите на Lambda
  • Внедряване и тест на API

2. Основи и условия

API Gateway е напълно управлявана услуга, която позволява на разработчиците да създават, публикуват, поддържат, наблюдават и защитават API във всякакъв мащаб .

Можем да приложим последователен и мащабируем HTTP-базиран интерфейс за програмиране (наричан още RESTful услуги) за достъп до бекенд услуги като Lambda функции, допълнителни AWS услуги (например EC2, S3, DynamoDB) и всякакви HTTP крайни точки .

Функциите включват, но не се ограничават до:

  • Управление на трафика
  • Разрешение и контрол на достъпа
  • Мониторинг
  • Управление на версията на API
  • Ограничаване на заявките за предотвратяване на атаки

Подобно на AWS Lambda, API Gateway се мащабира автоматично и се таксува за API повикване.

Подробна информация можете да намерите в официалната документация.

2.1. Условия

API шлюзът е услуга на AWS, която поддържа създаването, внедряването и управлението на интерфейс за програмиране на RESTful за излагане на вътрешни HTTP крайни точки, функции на AWS Lambda и други AWS услуги.

Един API Gateway API е колекция от ресурси и методи, които могат да бъдат интегрирани с Lambda функции, други AWS услуги, или HTTP крайни точки в гръб. API се състои от ресурси, които формират структурата на API. Всеки API ресурс може да изложи един или повече API методи, които трябва да имат уникални HTTP глаголи.

За да публикуваме API, трябва да създадем внедряване на API и да го свържем с така наречения етап . Етапът е като моментна снимка във времето на API. Ако преразпределим API, можем или да актуализираме съществуващ етап, или да създадем нов. С това, различни версии на API в същото време са възможни, например Дев етап на тест етап, а дори и на няколко стандартни версии, като v1 , v2 , и т.н.

Интеграцията на Lambda Proxy е опростена конфигурация за интеграция между функциите на Lambda и API Gateway.

API шлюзът изпраща цялата заявка като вход към вътрешна функция Lambda. Отговорно, API шлюзът трансформира изхода на функцията Lambda обратно към фронтен HTTP отговор.

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

Ще ни трябват същите зависимости, както в статията AWS Lambda Using DynamoDB With Java.

На всичкото отгоре се нуждаем и от JSON Simple библиотека:

 com.googlecode.json-simple json-simple 1.1.1 

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

В този раздел ще разработим и изградим нашите Lambda функции в Java, ще го внедрим с помощта на AWS Console и ще пуснем бърз тест.

Тъй като искаме да демонстрираме основните възможности за интегриране на API Gateway с Lambda, ще създадем две функции:

  • Функция 1: получава полезен товар от API, използвайки метод PUT
  • Функция 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 String.

4.2. Внедряване на RequestHandler Class

Точно както в статията 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

В първата ни функция искаме да демонстрираме как да получим полезен товар (като от заявка PUT или POST) от API шлюза :

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 proxy. Очакваме API шлюзът да предаде пълната заявка на функцията Lambda в параметъра InputStream .

All we have to do is to pick the relevant attributes from the contained JSON structure.

As we can see, the method basically consists of three steps:

  1. Fetching the body object from our input stream and creating a Person object from that
  2. Storing that Person object in a DynamoDB table
  3. Building a JSON object, which can hold several attributes, like a body for the response, custom headers, as well as an HTTP status code

One point worth mentioning here: API Gateway expects the body to be a String (for both request and response).

As we expect to get a String as body from the API Gateway, we cast the body to String and initialize our Person object:

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

API Gateway also expects the response body to be a String:

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

This topic is not mentioned explicitly in the official documentation. However, if we have a close look, we can see that the body attribute is a String in both snippets for the request as well as for the response.

The advantage should be clear: even if JSON is the format between API Gateway and the Lambda function, the actual body can contain plain text, JSON, XML, or whatever. It is then the responsibility of the Lambda function to handle the format correctly.

We'll see how the request and response body look later when we test our functions in the AWS Console.

The same also applies to the following two functions.

4.4. Implementation of Function 2

In a second step, we want to demonstrate how to use a path parameter or a query string parameter for retrieving a Person item from the database using its ID:

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(); }

Again, three steps are relevant:

  1. We check whether a pathParameters or an queryStringParameters array with an id attribute are present.
  2. If true, we use the belonging value to request a Person item with that ID from the database.
  3. We add a JSON representation of the received item to the response.

The official documentation provides a more detailed explanation of input format and output format for Proxy Integration.

4.5. Building Code

Again, we can simply build our code using Maven:

mvn clean package shade:shade

The JAR file will be created under the target folder.

4.6. Creating the DynamoDB Table

We can create the table as explained in AWS Lambda Using DynamoDB With Java.

Let's choose Person as table name, id as primary key name, and Number as type of the primary key.

4.7. Deploying Code via AWS Console

After building our code and creating the table, we can now create the functions and upload the code.

This can be done by repeating steps 1-5 from the AWS Lambda with Java article, one time for each of our two methods.

Let's use the following function names:

  • StorePersonFunction for the handleRequest method (function 1)
  • GetPersonByHTTPParamFunction for the handleGetByParam method (function 2)

We also have to define an environment variable TABLE_NAME with value “Person”.

4.8. Testing the Functions

Before continuing with the actual API Gateway part, we can run a quick test in the AWS Console, just to check that our Lambda functions are running correctly and can handle the Proxy Integration format.

Testing a Lambda function from the AWS Console works as described in AWS Lambda with Java article.

However, when we create a test event, we have to consider the special Proxy Integration format, which our functions are expecting. We can either use the API Gateway AWS Proxy template and customize that for our needs, or we can copy and paste the following events:

For the StorePersonFunction, we should use this:

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

As discussed before, the body must have the type String, even if containing a JSON structure. The reason is that the API Gateway will send its requests in the same format.

The following response should be returned:

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

Here, we can see that the body of our response is a String, although it contains a JSON structure.

Let's look at the input for the GetPersonByHTTPParamFunction.

For testing the path parameter functionality, the input would look like this:

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

And the input for sending a query string parameter would be:

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

As a response, we should get the following for both cases methods:

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

Again, the body is a String.

5. Creating and Testing the API

After we created and deployed the Lambda functions in the previous section, we can now create the actual API using the AWS Console.

Let's look at the basic workflow:

  1. Create an API in our AWS account.
  2. Add a resource to the resources hierarchy of the API.
  3. Create one or more methods for the resource.
  4. Set up the integration between a method and the belonging Lambda function.

We'll repeat steps 2-4 for each of our two functions in the following sections.

5.1. Creating the API

For creating the API, we'll have to:

  1. Sign in to the API Gateway console at //console.aws.amazon.com/apigateway
  2. Click on “Get Started” and then select “New API”
  3. Type in the name of our API (TestAPI) and acknowledge by clicking on “Create API”

Having created the API, we can now create the API structure and link it to our Lambda functions.

5.2. API Structure for Function 1

The following steps are necessary for our StorePersonFunction:

  1. Choose the parent resource item under the “Resources” tree and then select “Create Resource” from the “Actions” drop-down menu. Then, we have to do the following in the “New Child Resource” pane:
    • Type “Persons” as a name in the “Resource Name” input text field
    • Leave the default value in the “Resource Path” input text field
    • Choose “Create Resource”
  2. Choose the resource just created, choose “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose PUT from the HTTP method drop-down list and then choose the check mark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “StorePersonFunction” in “Lambda Function”
  3. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”

5.3. API Structure for Function 2 – Path Parameters

The steps for our retrieving path parameters are similar:

  1. Choose the /persons resource item under the “Resources” tree and then select “Create Resource” from the “Actions” drop-down menu. Then, we have to do the following in the New Child Resource pane:
    • Type “Person” as a name in the “Resource Name” input text field
    • Change the “Resource Path” input text field to “{id}”
    • Choose “Create Resource”
  2. Choose the resource just created, select “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose GET from the HTTP method drop-down list and then choose the check mark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “GetPersonByHTTPParamFunction” in “Lambda Function”
  3. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”

Note: it is important here to set the “Resource Path” parameter to “{id}”, as our GetPersonByPathParamFunction expects this parameter to be named exactly like this.

5.4. API Structure for Function 2 – Query String Parameters

The steps for receiving query string parameters are a bit different, as we don't have to create a resource, but instead have to create a query parameter for the id parameter:

  1. Choose the /persons resource item under the “Resources” tree, select “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose GET from the HTTP method drop-down list and then select the checkmark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “GetPersonByHTTPParamFunction” in “Lambda Function”.
  2. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”
  3. Choose “Method Request” on the right and carry out the following steps:
    • Expand the URL Query String Parameters list
    • Click on “Add Query String”
    • Type “id” in the name field, and choose the check mark icon to save
    • Select the “Required” checkbox
    • Click on the pen symbol next to “Request validator” on the top of the panel, select “Validate query string parameters and headers”, and choose the check mark icon

Note: It is important to set the “Query String” parameter to “id”, as our GetPersonByHTTPParamFunction expects this parameter to be named exactly like this.

5.5. Testing the API

Our API is now ready, but it's not public yet. Before we publish it, we want to run a quick test from the Console first.

For that, we can select the respective method to be tested in the “Resources” tree and click on the “Test” button. On the following screen, we can type in our input, as we would send it with a client via HTTP.

For StorePersonFunction, we have to type the following structure into the “Request Body” field:

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

For the GetPersonByHTTPParamFunction with path parameters, we have to type 2 as a value into the “{id}” field under “Path”.

For the GetPersonByHTTPParamFunction with query string parameters, we have to type id=2 as a value into the “{persons}” field under “Query Strings”.

5.6. Deploying the API

Up to now, our API wasn't public and thereby was only available from the AWS Console.

As discussed before, 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.

Let's see how the URL scheme for our API will look:

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

The following steps are required for deployment:

  1. Choose the particular API in the “APIs” navigation pane
  2. Choose “Actions” in the Resources navigation pane and select “Deploy API” from the “Actions” drop-down menu
  3. Choose “[New Stage]” from the “Deployment stage” drop-down, type “test” in “Stage name”, and optionally provide a description of the stage and deployment
  4. Trigger the deployment by choosing “Deploy”

After the last step, the console will provide the root URL of the API, for example, //0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test.

5.7. Invoking the Endpoint

As the API is public now, we can call it using any HTTP client we want.

With cURL, the calls would look like as follows.

StorePersonFunction:

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

GetPersonByHTTPParamFunction for path parameters:

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

GetPersonByHTTPParamFunction for query string parameters:

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

6. Conclusion

In this article, we had a look how to make AWS Lambda functions available as REST endpoints, using AWS API Gateway.

We explored the basic concepts and terminology of API Gateway, and we learned how to integrate Lambda functions using Lambda Proxy Integration.

Накрая видяхме как да създадем, внедрим и тестваме API.

Както обикновено, целият код за тази статия е достъпен в GitHub.