API GatewayでAWS Lambdaを使用する

API GatewayでAWS Lambdaを使用する

1. 概要

AWS Lambdaは、アマゾンウェブサービスが提供するサーバーレスコンピューティングサービスです。

以前の2つの記事では、how to create an AWS Lambda function using Javahow to access DynamoDB from a Lambda functionについて説明しました。

このチュートリアルでは、how to publish a Lambda function as a REST endpoint, using AWS Gatewayについて説明します。

次のトピックについて詳しく見ていきます。

  • API Gatewayの基本概念と用語

  • Lambdaプロキシ統合を使用したLambda関数とAPI Gatewayの統合

  • APIの作成、その構造、およびAPIリソースをLambda関数にマップする方法

  • APIの展開とテスト

2. 基本と用語

APIゲートウェイは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 Gatewayは自動的にスケールアウトされ、API呼び出しごとに請求されます。

詳細情報はofficial documentationにあります。

2.1. 条項

API Gatewayは、RESTfulアプリケーションプログラミングインターフェイスの作成、デプロイ、管理をサポートするAWSサービスであり、バックエンドHTTPエンドポイント、AWS Lambda関数、およびその他のAWSサービスを公開します。

API Gateway APIは、Lambda関数、他のAWSサービス、またはバックエンドのHTTPエンドポイントと統合できるリソースとメソッドのコレクションです。 APIは、API構造を形成するリソースで構成されます。 各APIリソースは、一意のHTTP動詞を持つ必要がある1つ以上のAPIメソッドを公開できます。

APIを公開するには、API deploymentを作成し、それをいわゆるstageに関連付ける必要があります。 ステージは、APIの時点でのスナップショットのようなものです。 APIを再デプロイする場合、既存のステージを更新するか、新しいステージを作成できます。 これにより、異なるバージョンのAPIを同時に使用できます。たとえば、devステージ、testステージ、さらにはv1v2などの複数の製品バージョンです。 sなど

Lambda Proxy integrationは、Lambda関数とAPIGatewayを統合するための簡略化された構成です。

API Gatewayは、リクエスト全体をバックエンドLambda関数への入力として送信します。 レスポンスに関しては、API GatewayはLambda関数の出力をフロントエンドHTTPレスポンスに変換します。

3. 依存関係

AWS Lambda Using DynamoDB With Javaの記事と同じ依存関係が必要になります。

その上、JSON Simpleライブラリも必要です。


    com.googlecode.json-simple
    json-simple
    1.1.1

4. Lambda関数の開発とデプロイ

このセクションでは、JavaでLambda関数を開発および構築し、AWSコンソールを使用してデプロイし、簡単なテストを実行します。

API GatewayをLambdaと統合する基本的な機能を示したいので、2つの関数を作成します。

  • Function 1:は、PUTメソッドを使用してAPIからペイロードを受信します

  • Function 2:は、APIからのHTTPパスパラメータまたはHTTPクエリパラメータの使用方法を示しています

実装に関しては、1つのRequestHandlerクラスを作成します。このクラスには、関数ごとに1つずつ、合計2つのメソッドがあります。

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
}

このモデルは、2つのプロパティを持つ1つの単純なPersonクラスで構成されています。 唯一の注目すべき部分は、JSON文字列を受け入れるPerson(String)コンストラクターです。

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()という1つのメソッドのみを定義します。 とにかく、ここで行ったように、同じクラスでさらに関数を定義できます。 もう1つのオプションは、関数ごとにRequestStreamHanderの実装を1つ作成することです。

特定のケースでは、簡単にするために前者を選択しました。 ただし、パフォーマンスやコードの保守性などの要因を考慮して、ケースバイケースで選択する必要があります。

また、TABLE_NAME environment変数からDynamoDBテーブルの名前を読み取ります。 その変数は、後で展開中に定義します。

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

前に説明したように、Lambdaプロキシ統合を使用するようにAPIを後で構成します。 API Gatewayは、InputStreamパラメーターで完全なリクエストをLambda関数に渡すことを期待しています。

含まれているJSON構造から関連する属性を選択するだけです。

ご覧のとおり、このメソッドは基本的に3つのステップで構成されています。

  1. 入力ストリームからbodyオブジェクトをフェッチし、そこからPersonオブジェクトを作成します

  2. そのPersonオブジェクトをDynamoDBテーブルに保存する

  3. 応答のbody、カスタムヘッダー、HTTPステータスコードなど、いくつかの属性を保持できるJSONオブジェクトを作成する

ここで言及する価値のある1つのポイント:API Gatewayは、bodyStringであることを想定しています(要求と応答の両方)。

API GatewayからStringbodyとして取得することを期待しているので、bodyStringにキャストし、Personオブジェクトを初期化します。

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

API Gatewayは、応答bodyStringであることも想定しています。

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

このトピックは公式ドキュメントでは明示的に言及されていません。 ただし、よく見ると、body属性はスニペットfor the requestfor the responseの両方でStringであることがわかります。

利点は明らかです。JSONがAPI GatewayとLambda関数の間の形式である場合でも、実際の本文にはプレーンテキスト、JSON、XMLなどを含めることができます。 その場合、Lambda関数は形式を正しく処理する必要があります。

後でAWSコンソールで関数をテストするときに、リクエストとレスポンスの本文がどのように見えるかを確認します。

以下の2つの機能にも同じことが当てはまります。

4.4. Implementation of Function 2

2番目のステップでは、IDを使用してデータベースからPersonアイテムを取得するためのhow to use a path parameter or a query string parameterを示します。

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

繰り返しますが、3つのステップが関連しています。

  1. pathParametersまたはid属性を持つqueryStringParameters配列が存在するかどうかを確認します。

  2. trueの場合、所属する値を使用して、データベースからそのIDを持つPersonアイテムを要求します。

  3. 受信したアイテムのJSON表現を応答に追加します。

公式ドキュメントには、プロキシ統合のinput formatoutput 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コンソールを介したコードのデプロイ

コードを構築してテーブルを作成したら、関数を作成してコードをアップロードできます。

これは、AWS Lambda with Javaの記事の手順1〜5を、2つの方法のそれぞれに対して1回繰り返すことで実行できます。

次の関数名を使用しましょう。

  • handleRequestメソッドのStorePersonFunction(関数1)

  • handleGetByParamメソッドのGetPersonByHTTPParamFunction (関数2)

また、値“Person”で環境変数TABLE_NAMEを定義する必要があります。

4.8. 機能のテスト

実際のAPI Gateway部分を続行する前に、AWSコンソールで簡単なテストを実行して、Lambda関数が正しく実行され、プロキシ統合形式を処理できることを確認できます。

AWSコンソールからのLambda関数のテストは、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は、JSON構造が含まれている場合でも、タイプStringである必要があります。 理由は、API Gatewayは同じ形式でリクエストを送信するためです。

次の応答が返されます。

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

ここでは、応答のbodyStringであることがわかりますが、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
}

繰り返しますが、bodyStringです。

5. APIの作成とテスト

前のセクションでLambda関数を作成してデプロイした後、we can now create the actual API using the AWS Console

基本的なワークフローを見てみましょう。

  1. AWSアカウントでAPIを作成します。

  2. APIのリソース階層にリソースを追加します。

  3. リソースの1つ以上のメソッドを作成します。

  4. メソッドと所属するLambda関数間の統合を設定します。

次のセクションでは、2つの機能のそれぞれについて手順2〜4を繰り返します。

5.1. APIの作成

APIを作成するには、次のことを行う必要があります。

  1. https://console.aws.amazon.com/apigatewayでAPIGatewayコンソールにサインインします

  2. 「はじめに」をクリックして、「新しいAPI」を選択します

  3. APIの名前(TestAPI)を入力し、[APIの作成]をクリックして確認します

APIを作成したら、API構造を作成してLambda関数にリンクできます。

5.2. 関数1のAPI構造

StorePersonFunctionには、次の手順が必要です。

  1. 「リソース」ツリーの下で親リソース項目を選択し、「アクション」ドロップダウンメニューから「リソースの作成」を選択します。 次に、「新しい子リソース」ペインで次の操作を行う必要があります。

    • 「リソース名」入力テキストフィールドに名前として「Persons」と入力します

    • [リソースパス]入力テキストフィールドの既定値のままにします。

    • [リソースの作成]を選択します

  2. 作成したばかりのリソースを選択し、「アクション」ドロップダウンメニューから「メソッドの作成」を選択し、次の手順を実行します。

    • [HTTPメソッド]ドロップダウンリストから[PUT]を選択し、チェックマークアイコンを選択して選択を保存します

    • 「Lambda Function」を統合タイプのままにして、「Use Lambda Proxy integration」オプションを選択します

    • 以前にLambda関数をデプロイした「Lambda Region」からリージョンを選択します

    • 「ラムダ関数」に“StorePersonFunction”と入力します

  3. 「保存」を選択し、OK「OK」で確認します「プロンプトが表示されたら「許可をLambda関数に追加」」

5.3. 関数2のAPI構造–パスパラメーター

パスパラメータを取得する手順は似ています。

  1. [リソース]ツリーで/personsリソース項目を選択し、[アクション]ドロップダウンメニューから[リソースの作成]を選択します。 次に、「新しい子リソース」ペインで以下を実行する必要があります。

    • [リソース名]入力テキストフィールドに名前として“Person”と入力します

    • [リソースパス]入力テキストフィールドを“{id}”に変更します

    • [リソースの作成]を選択します

  2. 作成したばかりのリソースを選択し、「アクション」ドロップダウンメニューから「メソッドの作成」を選択し、次の手順を実行します。

    • [HTTPメソッド]ドロップダウンリストから[GET]を選択し、チェックマークアイコンを選択して選択を保存します。

    • integration「Lambda Function」を統合タイプのままにして、「Use Lambda Proxy integration」オプションを選択します

    • 以前にLambda関数をデプロイした「Lambda Region」からリージョンを選択します

    • 「ラムダ関数」に“GetPersonByHTTPParamFunction”と入力します

  3. 「保存」を選択し、OK「OK」で確認します「プロンプトが表示されたら「許可をLambda関数に追加」」

注:ここでは、「リソースパス」パラメータを“{id}”に設定することが重要です。これは、GetPersonByPathParamFunction がこのパラメータにこのように正確に名前を付けることを想定しているためです。

5.4. 関数2のAPI構造–クエリ文字列パラメーター

クエリ文字列パラメータを受け取る手順は、we don’t have to create a resource, but instead have to create a query parameter for the id parameterのように少し異なります。

  1. [リソース]ツリーで/personsのリソース項目を選択し、[アクション]ドロップダウンメニューから[メソッドの作成]を選択して、次の手順を実行します。

    • [HTTPメソッド]ドロップダウンリストから[GET]を選択し、チェックマークアイコンを選択して選択を保存します

    • 「Lambda関数」を統合タイプのままにして、「使用Lambdaプロキシ統合」オプションを選択します

    • 以前にLambda関数をデプロイした「Lambda Region」からリージョンを選択します

    • 「ラムダ関数」に“GetPersonByHTTPParamFunction”と入力します。

  2. 「保存」を選択し、OK「OK」で確認します「プロンプトが表示されたら「許可をLambda関数に追加」」

  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の場合、[リクエスト本文]フィールドに次の構造を入力する必要があります。

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

パスパラメータを使用するGetPersonByHTTPParamFunctionの場合、「パス」の下の「{id}」フィールドに値として2を入力する必要があります。

クエリ文字列パラメータを使用するGetPersonByHTTPParamFunction の場合、[クエリ文字列]の下の[{persons}]フィールドに値としてid=2を入力する必要があります。

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

APIのURLスキームがどのように見えるか見てみましょう。

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

展開には次の手順が必要です。

  1. 「API」ナビゲーションペインで特定のAPIを選択します

  2. [リソース]ナビゲーションペインで[アクション]を選択し、[アクション]ドロップダウンメニューから[APIのデプロイ]を選択します。

  3. [導入ステージ]プルダウンから[[新しいステージ]]を選択し、[ステージ名]に“test”と入力し、オプションでステージと導入の説明を入力します

  4. [展開]を選択して、展開をトリガーします。

最後の手順の後、コンソールはAPIのルートURL(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 API Gatewayを使用して、AWS Lambda関数をRESTエンドポイントとして使用できるようにする方法について説明しました。

API Gatewayの基本概念と用語を調査し、Lambda Proxy Integrationを使用してLambda関数を統合する方法を学びました。

最後に、APIを作成、デプロイ、テストする方法を見ました。

いつものように、この記事のすべてのコードはon GitHubで利用できます。