aws api 网关和 lambda:多个 endpoint/functions 与单个端点

aws api gateway & lambda: multiple endpoint/functions vs single endpoint

我有一个代理 lamba 函数的 AWS api。我目前使用具有不同 lambda 函数的不同端点:

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

管理所有端点和功能的过程变得繁琐。当我将单个端点用于一个 lambda 函数时有什么缺点,它根据查询字符串决定要做什么吗?

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

将多个方法映射到单个 lambda 函数是完全有效的,如今许多人都在使用这种方法,而不是为每个离散方法创建一个 api 网关资源和 lambda 函数。

您可以考虑将所有请求代理到一个函数。查看以下有关创建 API 网关 => 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
  }
}

现在您可以访问所有 headers、url 参数、body 等,您可以使用它在单个 Lambda 函数中以不同方式处理请求(基本上实现您自己的路由)。

作为一种观点,我看到了这种方法的一些优点和缺点。其中许多取决于您的具体用例:

  • 部署:如果每个 lambda 函数都是离散的,那么您可以独立部署它们,这可能会降低代码更改的风险(微服务策略)。相反,您可能会发现需要单独部署功能会增加复杂性和负担。
  • 自我描述: API 网关的界面让您可以非常直观地查看 RESTful 端点的布局——名词和动词都是可见的乍看上去。实施您自己的路由可能会以牺牲这种可见性为代价。
  • Lambda 大小和限制:如果您代理所有 - 那么您最终需要选择一个实例大小、超时等,以适应您的所有 RESTful 端点。如果您创建离散函数,那么您可以更仔细地选择最能满足特定调用需求的内存占用、超时、死信行为等。

我已经使用 Lambda-API 网关构建了 5~6 个微服务,并经历了几次尝试、失败和成功。

简而言之,根据我的经验,最好只使用一个 API 网关通配符映射将所有 API 调用委托给 lambda,例如

/api/{proxy+} -> Lambda

如果您曾经使用过 grape 这样的框架,您就会知道在制作 API 时,
这样的功能 “中间件”
“全局异常处理”
“级联路由”
“参数验证”

真的很重要。 随着 API 的增长,几乎不可能使用 API 网关映射管理所有路由,API 网关也不支持这些功能。

此外,为开发或部署的每个端点打破 lambda 并不实际。

根据您的示例,

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 我们刚刚为 lambda 构建了 Web 框架,因为我的公司需要它。

我本来想为 Dave Maple's 很好的答案添加几点,但我还没有足够的声誉点,所以我会在这里添加评论。

我开始探索指向一个 Lambda 函数的多个端点的路径,该函数可以通过访问事件的 'resource' 属性 来区别对待每个端点。尝试之后,由于戴夫建议的原因,我现在将它们分成不同的功能加上:

  • 我发现当功能分开时,更容易查看日志和监视器。
  • 作为初学者,我一开始没有注意到的一个细微差别是,您可以拥有一个代码库并部署与多个 Lambda 函数完全相同的代码。这使您可以在代码库中享受功能分离的好处和整合方法的好处。
  • 您可以使用 AWS CLI 跨多个功能自动执行任务,以 reduce/eliminate 管理单独功能的缺点。例如,我有一个用相同代码更新 10 个函数的脚本。

据我所知,AWS 只允许每个 Lambda 函数有一个处理程序。这就是为什么我用 Java 泛型创建了一个 "routing" 机制(为了在编译时进行更强的类型检查)。在以下示例中,您可以 调用多个方法并将不同的对象类型传递给 Lambda 并通过一个 Lambda 处理程序返回 :

Lambda class 处理程序:

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

Lambda请求class:

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 class:

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 用户 class:

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函数中(因为uf 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);
}

在我看来,选择单个还是多个 API 是考虑以下因素的函数:

  1. 安全性:我认为这是拥有单一 API 结构的最大挑战。对于需求的不同部分可能有不同的安全配置文件

  2. 从业务角度思考微服务模型: 任何 API 的全部目的应该是服务于一些请求,因此它必须很好理解并且易于使用。所以相关的 API 应该合并。例如,如果您有一个移动客户端并且它需要从数据库中拉入和拉出 10 个东西,那么将 10 个端点放入一个 API 中是有意义的。 但这应该在合理范围内,并且应该在整体解决方案设计的上下文中看到。例如,如果你设计一个薪资产品,你可能会想有单独的模块用于休假管理和用户详细信息管理。即使它们经常被单个客户端使用,它们仍然应该是不同的API,因为它们的业务意义是不同的。

  3. 可重用性:适用于代码和功能的可重用性。代码可重用性是一个更容易解决的问题,即为共享需求构建通用模块并将它们构建为库。 功能可重用性更难解决。在我看来,大多数情况都可以通过重新设计 endpoints/functions 的布局方式来解决,因为如果您需要重复功能,则意味着您的初始设计不够详细。

刚刚在另一个 SO post 中找到了 link,它总结得更好

名为 Best practices for organizing larger serverless applications.

的官方 AWS 博文中提到了类似的情况

一般建议是将“单体 lambda”拆分为单独的 lambda,并将路由移动到 API 网关。

这是博客中关于“单体 lambda”方法的内容:

This approach is generally unnecessary, and it’s often better to take advantage of the native routing functionality available in API Gateway. ... API Gateway is also capable of validating parameters, reducing the need for checking parameters with custom code. It can also provide protection against unauthorized access, and a range of other features more suited to be handled at the service level.

从这里开始:

对此

将 API 请求映射到 AWS 中的 Lambda 的责任是通过网关的 API 规范处理的。

URL 路径和 HTTP 方法的映射以及数据验证应该留给网关。还有权限和API范围的问题;您将无法以正常方式利用 API 范围和 IAM 权限级别。

就编码而言,在 Lambda 处理程序中复制此机制是一种反模式。沿着这条路线走下去,很快就会得到一些看起来像 node express 服务器路由的东西,而不是 Lambda 函数。

在 API 网关后面设置了 50 多个 Lambda 之后,我可以说 函数处理程序应尽可能保留为转储,从而使它们可以独立于调用它们的上下文进行重用。