Verwenden von AWS Lambda mit API Gateway

1. Übersicht

AWS Lambda ist ein serverloser Computerdienst, der von Amazon Web Services bereitgestellt wird.

In zwei vorherigen Artikeln haben wir erläutert, wie Sie eine AWS Lambda-Funktion mit Java erstellen und wie Sie über eine Lambda-Funktion auf DynamoDB zugreifen.

In diesem Lernprogramm wird erläutert, wie Sie eine Lambda-Funktion als REST-Endpunkt mithilfe von AWS Gateway veröffentlichen .

Wir werden uns die folgenden Themen genauer ansehen:

  • Grundlegende Konzepte und Begriffe von API Gateway
  • Integration von Lambda-Funktionen mit API Gateway mithilfe der Lambda Proxy-Integration
  • Erstellung einer API, ihrer Struktur und Zuordnung der API-Ressourcen zu Lambda-Funktionen
  • Bereitstellung und Test der API

2. Grundlagen und Begriffe

API Gateway ist ein vollständig verwalteter Dienst, mit dem Entwickler APIs in jeder Größenordnung erstellen, veröffentlichen, warten, überwachen und sichern können .

Wir können eine konsistente und skalierbare HTTP-basierte Programmierschnittstelle (auch als RESTful-Services bezeichnet) implementieren, um auf Backend-Services wie Lambda-Funktionen, weitere AWS-Services (z. B. EC2, S3, DynamoDB) und beliebige HTTP-Endpunkte zuzugreifen .

Zu den Funktionen gehören unter anderem:

  • Verkehrsregelung
  • Autorisierung und Zugriffskontrolle
  • Überwachung
  • API-Versionsverwaltung
  • Drosselungsanforderungen, um Angriffe zu verhindern

Wie AWS Lambda wird API Gateway automatisch skaliert und pro API-Aufruf in Rechnung gestellt.

Detaillierte Informationen finden Sie in der offiziellen Dokumentation.

2.1. Bedingungen

API Gateway ist ein AWS-Service, der das Erstellen, Bereitstellen und Verwalten einer RESTful-Anwendungsprogrammierschnittstelle unterstützt, um Backend-HTTP-Endpunkte, AWS Lambda-Funktionen und andere AWS-Services verfügbar zu machen.

Eine API-Gateway-API ist eine Sammlung von Ressourcen und Methoden, die in Lambda-Funktionen, andere AWS-Services oder HTTP-Endpunkte im Backend integriert werden können. Die API besteht aus Ressourcen, die die API-Struktur bilden. Jede API-Ressource kann eine oder mehrere API-Methoden verfügbar machen, die eindeutige HTTP-Verben enthalten müssen.

Um eine API zu veröffentlichen, müssen wir eine API-Bereitstellung erstellen und sie einer sogenannten Stufe zuordnen . Eine Phase ist wie eine Momentaufnahme der API. Wenn wir eine API erneut bereitstellen, können wir entweder eine vorhandene Phase aktualisieren oder eine neue erstellen. Auf diese Weise sind verschiedene Versionen einer API gleichzeitig möglich, beispielsweise eine Entwicklungsphase , eine Testphase und sogar mehrere Produktionsversionen wie v1 , v2 usw.

Die Lambda-Proxy-Integration ist eine vereinfachte Konfiguration für die Integration zwischen Lambda-Funktionen und API-Gateway.

Das API-Gateway sendet die gesamte Anforderung als Eingabe an eine Backend-Lambda-Funktion. In Bezug auf die Antwort wandelt API Gateway die Ausgabe der Lambda-Funktion zurück in eine Frontend-HTTP-Antwort um.

3. Abhängigkeiten

Wir benötigen dieselben Abhängigkeiten wie im Artikel AWS Lambda Using DynamoDB With Java.

Darüber hinaus benötigen wir die JSON Simple-Bibliothek:

 com.googlecode.json-simple json-simple 1.1.1 

4. Entwickeln und Bereitstellen der Lambda-Funktionen

In diesem Abschnitt entwickeln und erstellen wir unsere Lambda-Funktionen in Java, stellen sie mit AWS Console bereit und führen einen kurzen Test durch.

Um die grundlegenden Funktionen der Integration von API Gateway in Lambda zu demonstrieren, erstellen wir zwei Funktionen:

  • Funktion 1: Empfängt eine Nutzlast von der API unter Verwendung einer PUT-Methode
  • Funktion 2: Veranschaulicht die Verwendung eines HTTP-Pfadparameters oder eines HTTP-Abfrageparameters, der von der API stammt

In Bezug auf die Implementierung erstellen wir eine RequestHandler- Klasse mit zwei Methoden - eine für jede Funktion.

4.1. Modell

Bevor wir den eigentlichen Request-Handler implementieren, werfen wir einen kurzen Blick auf unser Datenmodell:

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 }

Unser Modell besteht aus einer einfachen Personenklasse mit zwei Eigenschaften. Der einzige bemerkenswerte Teil ist der Konstruktor Person (String) , der einen JSON-String akzeptiert.

4.2. Implementierung der RequestHandler-Klasse

Genau wie im Artikel AWS Lambda With Java erstellen wir eine Implementierung der RequestStreamHandler- Schnittstelle:

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 } }

Wie wir sehen können, definiert die RequestStreamHander- Schnittstelle nur eine Methode, handeRequest () . Wie auch immer, wir können weitere Funktionen in derselben Klasse definieren, wie wir es hier getan haben. Eine andere Möglichkeit wäre, eine Implementierung von RequestStreamHander für jede Funktion zu erstellen .

In unserem speziellen Fall haben wir der Einfachheit halber den ersteren gewählt. Die Auswahl muss jedoch von Fall zu Fall getroffen werden, wobei Faktoren wie Leistung und Code-Wartbarkeit berücksichtigt werden.

Wir lesen auch den Namen unserer DynamoDB-Tabelle aus der Umgebungsvariablen TABLE_NAME . Wir werden diese Variable später während der Bereitstellung definieren.

4.3. Implementierung von Funktion 1

In unserer ersten Funktion möchten wir zeigen, wie eine Nutzlast (wie von einer PUT- oder POST-Anforderung) vom API-Gateway abgerufen wird :

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

Wie bereits erwähnt, konfigurieren wir die API später für die Verwendung der Lambda-Proxy-Integration. Wir erwarten, dass das API-Gateway die vollständige Anforderung an die Lambda-Funktion im InputStream- Parameter übergibt .

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.

Schließlich haben wir gesehen, wie eine API erstellt, bereitgestellt und getestet wird.

Wie üblich ist der gesamte Code für diesen Artikel auf GitHub verfügbar.