Utilisation d'AWS Lambda avec API Gateway

1. Vue d'ensemble

AWS Lambda est un service informatique sans serveur fourni par Amazon Web Services.

Dans deux articles précédents, nous avons expliqué comment créer une fonction AWS Lambda à l'aide de Java, ainsi que comment accéder à DynamoDB à partir d'une fonction Lambda.

Dans ce didacticiel, nous verrons comment publier une fonction Lambda en tant que point de terminaison REST, à l'aide d'AWS Gateway .

Nous examinerons en détail les sujets suivants:

  • Concepts et conditions de base d'API Gateway
  • Intégration des fonctions Lambda avec API Gateway à l'aide de l'intégration du proxy Lambda
  • Création d'une API, sa structure et comment mapper les ressources de l'API sur des fonctions Lambda
  • Déploiement et test de l'API

2. Principes de base et conditions

API Gateway est un service entièrement géré qui permet aux développeurs de créer, publier, maintenir, surveiller et sécuriser des API à n'importe quelle échelle .

Nous pouvons implémenter une interface de programmation HTTP cohérente et évolutive (également appelée services RESTful) pour accéder aux services backend tels que les fonctions Lambda, à d'autres services AWS (par exemple, EC2, S3, DynamoDB) et à tous les points de terminaison HTTP .

Les fonctionnalités incluent, mais ne sont pas limitées à:

  • Gestion du trafic
  • Autorisation et contrôle d'accès
  • surveillance
  • Gestion des versions d'API
  • Limitation des demandes pour empêcher les attaques

Comme AWS Lambda, API Gateway est automatiquement dimensionnée et facturée par appel d'API.

Des informations détaillées peuvent être trouvées dans la documentation officielle.

2.1. termes

API Gateway est un service AWS qui prend en charge la création, le déploiement et la gestion d'une interface de programmation d'application RESTful pour exposer les points de terminaison HTTP backend, les fonctions AWS Lambda et d'autres services AWS.

Une API API Gateway est un ensemble de ressources et de méthodes qui peuvent être intégrées aux fonctions Lambda, à d'autres services AWS ou aux points de terminaison HTTP dans le backend. L'API se compose de ressources qui forment la structure de l'API. Chaque ressource API peut exposer une ou plusieurs méthodes API qui doivent avoir des verbes HTTP uniques.

Pour publier une API, nous devons créer un déploiement d'API et l'associer à une soi-disant étape . Une étape est comme un instantané dans le temps de l'API. Si nous redéployons une API, nous pouvons soit mettre à jour une étape existante, soit en créer une nouvelle. En cela, les différentes versions d'une API en même temps sont possibles, par exemple un dev stade, un test de scène, et même les versions de production multiples, comme v1 , v2 , etc.

L'intégration du proxy Lambda est une configuration simplifiée pour l'intégration entre les fonctions Lambda et API Gateway.

La passerelle API envoie la requête entière en tant qu'entrée à une fonction Lambda backend. En termes de réponse, API Gateway transforme la sortie de la fonction Lambda en une réponse HTTP frontend.

3. Dépendances

Nous aurons besoin des mêmes dépendances que dans l'article AWS Lambda utilisant DynamoDB avec Java.

En plus de cela, nous avons également besoin de la bibliothèque JSON Simple:

 com.googlecode.json-simple json-simple 1.1.1 

4. Développement et déploiement des fonctions Lambda

Dans cette section, nous développerons et créerons nos fonctions Lambda en Java, nous les déploierons à l'aide d'AWS Console et nous exécuterons un test rapide.

Comme nous voulons démontrer les capacités de base de l'intégration d'API Gateway avec Lambda, nous allons créer deux fonctions:

  • Fonction 1: reçoit une charge utile de l'API, en utilisant une méthode PUT
  • Fonction 2: montre comment utiliser un paramètre de chemin HTTP ou un paramètre de requête HTTP provenant de l'API

En ce qui concerne l' implémentation, nous allons créer une classe RequestHandler , qui a deux méthodes - une pour chaque fonction.

4.1. Modèle

Avant d'implémenter le gestionnaire de requêtes réel, jetons un coup d'œil à notre modèle de données:

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 }

Notre modèle se compose d'une classe Person simple , qui a deux propriétés. La seule partie notable est le constructeur Person (String) , qui accepte une chaîne JSON.

4.2. Implémentation de la classe RequestHandler

Tout comme dans l'article AWS Lambda avec Java, nous allons créer une implémentation de l' interface 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 } }

Comme nous pouvons le voir, l' interface RequestStreamHander ne définit qu'une seule méthode, handeRequest () . Quoi qu'il en soit, nous pouvons définir d'autres fonctions dans la même classe, comme nous l'avons fait ici. Une autre option serait de créer une implémentation de RequestStreamHander pour chaque fonction.

Dans notre cas particulier, nous avons choisi le premier pour la simplicité. Cependant, le choix doit être fait au cas par cas, en tenant compte de facteurs tels que les performances et la maintenabilité du code.

Nous lisons également le nom de notre table DynamoDB à partir de la variable d'environnement TABLE_NAME . Nous définirons cette variable plus tard lors du déploiement.

4.3. Mise en œuvre de la fonction 1

Dans notre première fonction, nous voulons montrer comment obtenir une charge utile (comme à partir d'une requête PUT ou POST) à partir de la passerelle 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(); }

Comme indiqué précédemment, nous configurerons l'API ultérieurement pour utiliser l'intégration du proxy Lambda. Nous nous attendons à ce que la passerelle API transmette la requête complète à la fonction Lambda dans le paramètre InputStream .

Tout ce que nous avons à faire est de sélectionner les attributs pertinents dans la structure JSON contenue.

Comme nous pouvons le voir, la méthode se compose essentiellement de trois étapes:

  1. Récupérer l' objet body de notre flux d'entrée et créer un objet Person à partir de celui-ci
  2. Stockage de cet objet Person dans une table DynamoDB
  3. Construire un objet JSON, qui peut contenir plusieurs attributs, comme un corps pour la réponse, des en-têtes personnalisés, ainsi qu'un code d'état HTTP

Un point à mentionner ici: API Gateway s'attend à ce que le corps soit une chaîne (à la fois pour la demande et la réponse).

Comme nous nous attendons à obtenir un String comme corps de la passerelle API, nous convertissons le corps en String et initialisons notre objet Person :

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

API Gateway s'attend également à ce que le corps de la réponse soit une chaîne :

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

Ce sujet n'est pas mentionné explicitement dans la documentation officielle. Cependant, si nous y regardons de plus près, nous pouvons voir que l'attribut body est une chaîne dans les deux extraits de code pour la requête ainsi que pour la réponse.

L'avantage doit être clair: même si JSON est le format entre API Gateway et la fonction Lambda, le corps réel peut contenir du texte brut, JSON, XML ou autre. Il est alors de la responsabilité de la fonction Lambda de gérer correctement le format.

Nous verrons à quoi ressemblent la requête et le corps de la réponse plus tard, lorsque nous testerons nos fonctions dans AWS Console.

Il en va de même pour les deux fonctions suivantes.

4.4. Mise en œuvre de la fonction 2

Dans un deuxième temps, nous souhaitons montrer comment utiliser un paramètre de chemin ou un paramètre de chaîne de requête pour récupérer un élément Person de la base de données à l'aide de son 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(); }

Encore une fois, trois étapes sont pertinentes:

  1. Nous vérifions si un tableau pathParameters ou queryStringParameters avec un attribut id est présent.
  2. Si vrai , nous utilisons la valeur d'appartenance pour demander un élément Personne avec cet ID à partir de la base de données.
  3. Nous ajoutons une représentation JSON de l'élément reçu à la réponse.

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.

Enfin, nous avons vu comment créer, déployer et tester une API.

Comme d'habitude, tout le code de cet article est disponible à l'adresse over sur GitHub.