web-dev-qa-db-ja.com

AWS APIゲートウェイとラムダ:複数のエンドポイント/機能と単一のエンドポイント

Lamba機能をプロキシするAWS APIを持っています。私は現在、別々のラムダ関数で異なるエンドポイントを使用しています:

api.com/getData --> getData
api.com/addData --> addData
api.com/signUp --> signUp

すべてのエンドポイントと機能を管理するプロセスは面倒になります。クエリ文字列に基づいて何を行うかを決定する1つのラムダ関数に単一のエンドポイントを使用する場合、デメリットはありますか?

api.com/exec&func=getData --> exec --> if(params.func === 'getData') { ... }
36
Chris

複数のメソッドを単一のラムダ関数にマッピングすることは完全に有効であり、多くの人々は現在、個々のメソッドごとにAPIゲートウェイリソースとラムダ関数を作成するのではなく、この方法論を使用しています。

すべてのリクエストを単一の関数にプロキシすることを検討することもできます。 API Gatewayの作成に関する次のドキュメントをご覧ください=> Lambdaプロキシ統合: http://docs.aws.Amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple- proxy.html

彼らの例はここで素晴らしいです。次のようなリクエスト:

POST /testStage/hello/world?name=me HTTP/1.1
Host: gy415nuibc.execute-api.us-east-1.amazonaws.com
Content-Type: application/json
headerName: headerValue

{
    "a": 1
}

次のイベントデータをAWS Lambda関数に送信します。

{
  "message": "Hello me!",
  "input": {
    "resource": "/{proxy+}",
    "path": "/hello/world",
    "httpMethod": "POST",
    "headers": {
      "Accept": "*/*",
      "Accept-Encoding": "gzip, deflate",
      "cache-control": "no-cache",
      "CloudFront-Forwarded-Proto": "https",
      "CloudFront-Is-Desktop-Viewer": "true",
      "CloudFront-Is-Mobile-Viewer": "false",
      "CloudFront-Is-SmartTV-Viewer": "false",
      "CloudFront-Is-Tablet-Viewer": "false",
      "CloudFront-Viewer-Country": "US",
      "Content-Type": "application/json",
      "headerName": "headerValue",
      "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
      "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
      "User-Agent": "PostmanRuntime/2.4.5",
      "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
      "X-Forwarded-For": "54.240.196.186, 54.182.214.83",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    },
    "queryStringParameters": {
      "name": "me"
    },
    "pathParameters": {
      "proxy": "hello/world"
    },
    "stageVariables": {
      "stageVariableName": "stageVariableValue"
    },
    "requestContext": {
      "accountId": "12345678912",
      "resourceId": "roq9wj",
      "stage": "testStage",
      "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33",
      "identity": {
        "cognitoIdentityPoolId": null,
        "accountId": null,
        "cognitoIdentityId": null,
        "caller": null,
        "apiKey": null,
        "sourceIp": "192.168.196.186",
        "cognitoAuthenticationType": null,
        "cognitoAuthenticationProvider": null,
        "userArn": null,
        "userAgent": "PostmanRuntime/2.4.5",
        "user": null
      },
      "resourcePath": "/{proxy+}",
      "httpMethod": "POST",
      "apiId": "gy415nuibc"
    },
    "body": "{\r\n\t\"a\": 1\r\n}",
    "isBase64Encoded": false
  }
}

これで、すべてのヘッダー、URLパラメーター、ボディなどにアクセスできるようになり、それを使用して、1つのLambda関数でリクエストを異なる方法で処理できます(基本的に独自のルーティングを実装します)。

意見として、このアプローチにはいくつかの利点と欠点があります。それらの多くは、特定のユースケースに依存しています。

  • Deployment:各ラムダ関数が個別の場合、それらを個別にデプロイできます。これにより、コード変更によるリスクを減らすことができます(マイクロサービス戦略)。逆に、機能を個別に展開する必要があると、複雑さが増し、負担が大きくなることがあります。
  • 自己記述:API Gatewayのインターフェースにより、RESTfulエンドポイントのレイアウトを非常に直感的に確認できます-名詞と動詞はすべて一目でわかります。独自のルーティングを実装すると、この可視性が犠牲になります。
  • ラムダのサイジングと制限:すべてをプロキシする場合は、すべてに対応するインスタンスサイズ、タイムアウトなどを選択する必要がありますRESTfulエンドポイント。個別の関数を作成する場合、特定の呼び出しのニーズに最適なメモリフットプリント、タイムアウト、デッドレター動作などをより慎重に選択できます。
38
Dave Maple

私はLambda-API Gatewayで5〜6個のマイクロサービスを構築してきましたが、いくつかの試行錯誤と成功を経験しました。

要するに、私の経験から、すべてのAPI呼び出しをラムダに1つのAPIGatewayワイルドカードマッピングだけで委任する方が良いでしょう。

/api/{+proxy} -> Lambda

grape のようなフレームワークを使用したことがあるなら、APIを作成するとき、
「ミドルウェア」
「グローバル例外処理」
「カスケードルーティング」
"パラメータ検証"

は非常に重要です。 APIが大きくなると、API Gatewayマッピングですべてのルートを管理することはほぼ不可能になり、API Gatewayはこれらの機能以外もサポートしません。

さらに、開発または展開のためにエンドポイントごとにラムダを分割することは実際には現実的ではありません。

あなたの例から、

api.com/getData --> getData  
api.com/addData --> addData  
api.com/signUp --> signUp  

データORM、ユーザー認証ロジック、共通ビューファイル(data.erbなど)があるとします。それをどのように共有しますか?

あなたは

api/auth/{+proxy} -> AuthServiceLambda  
api/data/{+proxy} -> DataServiceLambda  

「エンドポイントごと」ではありません。マイクロサービスの概念と、サービスの分割方法に関するベストプラクティスを参照できます。

機能のようなWebフレームワークの場合、チェックアウト this 私は会社でこれが必要だったので、ラムダ用のWebフレームワークを作成しました。

13
Kurt Lee

Dave Maple's great answerにいくつかのポイントを追加するようにコメントしますが、まだ十分な評価ポイントがないため、ここにコメントを追加します。

イベントの「リソース」プロパティにアクセスすることで、各エンドポイントを別々に処理できる1つのLambda関数を指す複数のエンドポイントのパスを探し始めました。試した後、Daveが提案した理由により、それらを別々の関数に分離しました。

  • 機能が分離されている場合、ログとモニターを簡単に確認できます。
  • 初心者として最初に取り上げなかった1つのニュアンスは、1つのコードベースを持ち、複数のLambda関数とまったく同じコードをデプロイできることです。これにより、コードベースで機能分離の利点と統合アプローチの利点を得ることができます。
  • AWS CLIを使用して、複数の機能にわたるタスクを自動化して、個別の機能を管理することのマイナス面を軽減または排除できます。たとえば、同じコードで10個の関数を更新するスクリプトがあります。
8
Inspector6

私の知る限り、AWSはLambda関数ごとに1つのハンドラーのみを許可します。だからこそ、私はJava Generics(コンパイル時のより強力な型チェックのために)で小さな「ルーティング」メカニズムを作成しました。次の例では複数のメソッドを呼び出し、異なるオブジェクトを渡すLambdaに入力し、1つのLambdaハンドラーを介して戻ります

ハンドラーを含むLambdaクラス:

public class GenericLambda implements RequestHandler<LambdaRequest<?>, LambdaResponse<?>> {

@Override
public LambdaResponse<?> handleRequest(LambdaRequest<?> lambdaRequest, Context context) {

    switch (lambdaRequest.getMethod()) {
    case WARMUP:
        context.getLogger().log("Warmup");  
        LambdaResponse<String> lambdaResponseWarmup = new LambdaResponse<String>();
        lambdaResponseWarmup.setResponseStatus(LambdaResponse.ResponseStatus.IN_PROGRESS);
        return lambdaResponseWarmup;
    case CREATE:
        User user = (User)lambdaRequest.getData();
        context.getLogger().log("insert user with name: " + user.getName());  //insert user in db
        LambdaResponse<String> lambdaResponseCreate = new LambdaResponse<String>();
        lambdaResponseCreate.setResponseStatus(LambdaResponse.ResponseStatus.COMPLETE);
        return lambdaResponseCreate;
    case READ:
        context.getLogger().log("read user with id: " + (Integer)lambdaRequest.getData());
        user = new User(); //create user object for test, instead of read from db
        user.setName("name");
        LambdaResponse<User> lambdaResponseRead = new LambdaResponse<User>();
        lambdaResponseRead.setData(user);
        lambdaResponseRead.setResponseStatus(LambdaResponse.ResponseStatus.COMPLETE);
        return lambdaResponseRead;
    default:
        LambdaResponse<String> lambdaResponseIgnore = new LambdaResponse<String>();
        lambdaResponseIgnore.setResponseStatus(LambdaResponse.ResponseStatus.IGNORED);
        return lambdaResponseIgnore;    
    }
}
}

LambdaRequestクラス:

public class LambdaRequest<T> {
private Method method;
private T data;
private int languageID; 

public static enum Method {
    WARMUP, CREATE, READ, UPDATE, DELETE 
}

public LambdaRequest(){
}

public Method getMethod() {
    return method;
}
public void setMethod(Method create) {
    this.method = create;
}
public T getData() {
    return data;
}
public void setData(T data) {
    this.data = data;
}
public int getLanguageID() {
    return languageID;
}
public void setLanguageID(int languageID) {
    this.languageID = languageID;
}
}

LambdaResponseクラス:

public class LambdaResponse<T> {

private ResponseStatus responseStatus;
private T data;
private String errorMessage;

public LambdaResponse(){
}

public static enum ResponseStatus {
    IGNORED, IN_PROGRESS, COMPLETE, ERROR, COMPLETE_DUPLICATE
}

public ResponseStatus getResponseStatus() {
    return responseStatus;
}

public void setResponseStatus(ResponseStatus responseStatus) {
    this.responseStatus = responseStatus;
}

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

public String getErrorMessage() {
    return errorMessage;
}

public void setErrorMessage(String errorMessage) {
    this.errorMessage = errorMessage;
}

}

POJOユーザークラスの例:

public class User {
private String name;

public User() {
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
}

JUnitテストメソッド:

    @Test
public void GenericLambda() {
    GenericLambda handler = new GenericLambda();
    Context ctx = createContext();

    //test WARMUP
    LambdaRequest<String> lambdaRequestWarmup = new LambdaRequest<String>();
    lambdaRequestWarmup.setMethod(LambdaRequest.Method.WARMUP);
    LambdaResponse<String> lambdaResponseWarmup = (LambdaResponse<String>) handler.handleRequest(lambdaRequestWarmup, ctx);

    //test READ user
    LambdaRequest<Integer> lambdaRequestRead = new LambdaRequest<Integer>();
    lambdaRequestRead.setData(1); //db id
    lambdaRequestRead.setMethod(LambdaRequest.Method.READ);
    LambdaResponse<User> lambdaResponseRead = (LambdaResponse<User>) handler.handleRequest(lambdaRequestRead, ctx);
    }

ps .: 逆シリアル化の問題LinkedTreeMapにキャストできない場合)Lambda関数内(Generics/Gsonの場合)、次のステートメントを使用します。

YourObject yourObject = (YourObject)convertLambdaRequestData2Object(lambdaRequest, YourObject.class);

方法:

private <T> Object convertLambdaRequestData2Object(LambdaRequest<?> lambdaRequest, Class<T> clazz) {

    Gson gson = new Gson();
    String json = gson.toJson(lambdaRequest.getData());
    return gson.fromJson(json, clazz);
}
2
Jörg

私が見るように、単一または複数のAPIを選択することは、次の考慮事項の機能です。

  1. セキュリティ:これは、単一のAPI構造を持つことの最大の課題だと思います。要件の異なる部分に異なるセキュリティプロファイルを設定することも可能です。

  2. ビジネスの観点からマイクロサービスモデルを考える:APIの全体的な目的はいくつかのリクエストに対応する必要があるため、十分に理解され、使いやすい必要があります。したがって、関連するAPIを組み合わせる必要があります。たとえば、モバイルクライアントを使用していて、DBから10個のデータを取得および取得する必要がある場合、10個のエンドポイントを1つのAPIに含めることは理にかなっています。しかし、これは理にかなっているはずであり、全体的なソリューション設計のコンテキストで見られるべきです。たとえば、給与計算製品を設計する場合、休暇管理とユーザー詳細管理用に別々のモジュールがあると考えるかもしれません。 1つのクライアントで頻繁に使用される場合でも、ビジネス上の意味が異なるため、異なるAPIである必要があります。

  3. 再利用性:コードと機能の両方の再利用性に適用されます。コードの再利用性は、簡単に解決できる問題です。つまり、共有要件の共通モジュールをビルドし、ライブラリとしてビルドします。機能の再利用性は解決が困難です。私の考えでは、機能の重複が必要な場合、初期設計が十分に詳細ではないことを意味するため、エンドポイント/機能のレイアウト方法を再設計することで、ほとんどのケースを解決できます。

リンク を別のSO

0
Ayan Guha