Utilizzo di AWS Lambda con API Gateway

1. Panoramica

AWS Lambda è un servizio di elaborazione serverless fornito da Amazon Web Services.

In due articoli precedenti, abbiamo discusso come creare una funzione AWS Lambda utilizzando Java, nonché come accedere a DynamoDB da una funzione Lambda.

In questo tutorial, discuteremo come pubblicare una funzione Lambda come endpoint REST, utilizzando AWS Gateway .

Daremo uno sguardo dettagliato ai seguenti argomenti:

  • Concetti e termini di base di API Gateway
  • Integrazione delle funzioni Lambda con API Gateway utilizzando l'integrazione Lambda Proxy
  • Creazione di un'API, sua struttura e come mappare le risorse API sulle funzioni Lambda
  • Distribuzione e test dell'API

2. Nozioni di base e termini

API Gateway è un servizio completamente gestito che consente agli sviluppatori di creare, pubblicare, mantenere, monitorare e proteggere le API su qualsiasi scala .

Possiamo implementare un'interfaccia di programmazione basata su HTTP coerente e scalabile (denominata anche servizi RESTful) per accedere a servizi di backend come funzioni Lambda, altri servizi AWS (ad esempio, EC2, S3, DynamoDB) e qualsiasi endpoint HTTP .

Le funzionalità includono, ma non sono limitate a:

  • Gestione del traffico
  • Autorizzazione e controllo accessi
  • Monitoraggio
  • Gestione delle versioni API
  • Limitazione delle richieste per prevenire gli attacchi

Come AWS Lambda, API Gateway viene ridimensionato automaticamente e fatturato per chiamata API.

Informazioni dettagliate possono essere trovate nella documentazione ufficiale.

2.1. Termini

API Gateway è un servizio AWS che supporta la creazione, la distribuzione e la gestione di un'interfaccia di programmazione di applicazioni RESTful per esporre endpoint HTTP back-end, funzioni AWS Lambda e altri servizi AWS.

Un gateway API API è una raccolta di risorse e metodi che possono essere integrati con le funzioni lambda, altri servizi AWS, o HTTP endpoint nel backend. L'API è costituita da risorse che formano la struttura dell'API. Ogni risorsa API può esporre uno o più metodi API che devono avere verbi HTTP univoci.

Per pubblicare un'API, dobbiamo creare una distribuzione API e associarla a una cosiddetta fase . Una fase è come un'istantanea nel tempo dell'API. Se ridistribuiamo un'API, possiamo aggiornare una fase esistente o crearne una nuova. Con questo, differenti versioni di un'API contemporaneamente sono possibili, per esempio un dev fase, un test di fase, e anche più versioni di produzione, come v1 , v2 , etc.

L'integrazione Lambda Proxy è una configurazione semplificata per l'integrazione tra le funzioni Lambda e API Gateway.

Il gateway API invia l'intera richiesta come input a una funzione Lambda di backend. Dal punto di vista della risposta, API Gateway trasforma l'output della funzione Lambda in una risposta HTTP front-end.

3. Dipendenze

Avremo bisogno delle stesse dipendenze dell'articolo AWS Lambda Utilizzo di DynamoDB con Java.

Inoltre, abbiamo anche bisogno della libreria JSON Simple:

 com.googlecode.json-simple json-simple 1.1.1 

4. Sviluppo e distribuzione delle funzioni Lambda

In questa sezione svilupperemo e creeremo le nostre funzioni Lambda in Java, le distribuiremo utilizzando la console AWS e eseguiremo un rapido test.

Poiché vogliamo dimostrare le capacità di base dell'integrazione di API Gateway con Lambda, creeremo due funzioni:

  • Funzione 1: riceve un payload dall'API, utilizzando un metodo PUT
  • Funzione 2: dimostra come utilizzare un parametro di percorso HTTP o un parametro di query HTTP proveniente dall'API

Dal punto di vista dell'implementazione , creeremo una classe RequestHandler , che ha due metodi, uno per ogni funzione.

4.1. Modello

Prima di implementare l'effettivo gestore delle richieste, diamo una rapida occhiata al nostro modello di dati:

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 }

Il nostro modello è costituito da una semplice classe Person , che ha due proprietà. L'unica parte degna di nota è il costruttore Person (String) , che accetta una stringa JSON.

4.2. Implementazione della classe RequestHandler

Proprio come nell'articolo AWS Lambda con Java, creeremo un'implementazione dell'interfaccia 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 } }

Come possiamo vedere, l' interfaccia RequestStreamHander definisce un solo metodo, handeRequest () . Ad ogni modo, possiamo definire ulteriori funzioni nella stessa classe, come abbiamo fatto qui. Un'altra opzione potrebbe essere quella di creare un'implementazione di RequestStreamHander per ogni funzione.

Nel nostro caso specifico, abbiamo scelto il primo per semplicità. Tuttavia, la scelta deve essere fatta caso per caso, prendendo in considerazione fattori come le prestazioni e la manutenibilità del codice.

Abbiamo anche letto il nome della nostra tavola DynamoDB dal TABLE_NAME variabile d'ambiente. Definiremo quella variabile in seguito durante la distribuzione.

4.3. Implementazione della funzione 1

Nella nostra prima funzione, vogliamo dimostrare come ottenere un payload (come da una richiesta PUT o POST) da 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(); }

Come discusso in precedenza, configureremo l'API in un secondo momento per utilizzare l'integrazione del proxy Lambda. Ci aspettiamo che API Gateway passi la richiesta completa alla funzione Lambda nel parametro InputStream .

Tutto quello che dobbiamo fare è scegliere gli attributi rilevanti dalla struttura JSON contenuta.

Come possiamo vedere, il metodo consiste fondamentalmente in tre passaggi:

  1. Recupero dell'oggetto body dal nostro flusso di input e creazione di un oggetto Person da quello
  2. Memorizzare quell'oggetto Person in una tabella DynamoDB
  3. Creazione di un oggetto JSON, che può contenere diversi attributi, come un corpo per la risposta, intestazioni personalizzate e un codice di stato HTTP

Un punto degno di nota qui: API Gateway si aspetta che il corpo sia una stringa (sia per la richiesta che per la risposta).

Poiché ci aspettiamo di ottenere una stringa come corpo dal gateway API, eseguiamo il cast del corpo su String e inizializziamo il nostro oggetto Person :

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

API Gateway prevede anche che il corpo della risposta sia una stringa :

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

Questo argomento non è menzionato esplicitamente nella documentazione ufficiale. Tuttavia, se osserviamo attentamente, possiamo vedere che l'attributo body è una stringa in entrambi gli snippet per la richiesta e per la risposta.

Il vantaggio dovrebbe essere chiaro: anche se JSON è il formato tra API Gateway e la funzione Lambda, il corpo effettivo può contenere testo normale, JSON, XML o altro. È quindi responsabilità della funzione Lambda gestire correttamente il formato.

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.

Infine, abbiamo visto come creare, distribuire e testare un'API.

Come al solito, tutto il codice per questo articolo è disponibile su GitHub.