serverless / serverless

⚡ Serverless Framework – Use AWS Lambda and other managed cloud services to build apps that auto-scale, cost nothing when idle, and boast radically low maintenance.

Home Page:https://serverless.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Supporting PROXY Integration for APIG

flomotlik opened this issue · comments

This is a Feature Proposal

Description

AWS recently released their new Proxy integration for Lambda. It is a vastly simpler way to configure your your APIG->Lambda integration. You can read more details in the docs

Basically what they've implemented is a Lambda integration that makes request and response mappings unnecessary, so your HTTP request gets always mapped in a standard way to Lambda. In your response you provider a Hash like in the following example that will be automatically mapped to your response code, headers and body:

var response = {
        statusCode: responseCode,
        headers: {
            "x-custom-header" : "my custom header value"
        },
        body: JSON.stringify(responseBody)
    };
    context.succeed(response);

This is vastly simpler than the existing lambda integration.

That simplicity on the other hand also takes away some of the features and more control that we've implemented or discussed recently in Serverless. A proxy Lambda integration does not allow to set any custom integration response templates, it does not allow to set headers on the integration itself (making CORS how we implemented for example impossible without changing your own code).

While I do see the Lambda_proxy functionality as a win for a much easier implementation of apig->lambda I worry that if we only implement support for one we'll exclude some use cases, e.g. when you want to set up custom headers across various resources and don't want to have to put those headers into your code, but just in configuration so you can share the config easily thorugh serverless variables.

Proposal

My current idea is that instead of choosing either one of the two we support both. We'll add a proxy field to the http event that is true by default and will use the proxy integration from lambda, meaning for most simple use cases you can very easily set response codes or headers for your functions.

If you have a more complex case and want to have the full range of functionality the AWS Lambda integration provides you can set proxy: false and use all the request/response config we've recently implemented or which is currently being built in various PRs

/cc @serverless/vip @serverless/core

Similar or dependent issues:

Completely agree!

A proxy Lambda integration does not allow to set any custom integration response templates

I don't really think this is an issue imo. We can still have "response templates" (or whatever we can call them), but at the code level. Let's not forget that Serverless allows sharing code between functions. So if the logic responsible for "response templates" lives in the lib folder or something, it can be used in all functions. I'd personally prefer that over configuration any day.

when you want to set up custom headers across various resources and don't want to have to put those headers into your code, but just in configuration so you can share the config easily through serverless variables.

Is there a use case that forces a user to use configuration templates rather than code templates as described in the previous point? Maybe I haven't looked into it long enough, but I don't see anything that can be done with the old integration, but not with the new proxy integration.

I think as a framework we should provide only the easiest way to do things (as long as they're not limited of course). In this case, managing requests/responses at the code level is (and I think many people would agree) MUCH easier than in the config level using the tedious VTL, at least because they'll be managed using JS or any other user preferred language.

So I'd vote for only supporting the Lambda Proxy integration before we release v1.0 as long as it's not limiting some essential use cases.

Also, @flomotlik mentioned a good point earlier: switching between both implementations using the proxy: true/false described above means that users also have to change their handler code accordingly, or at least make it work for both cases (which makes the handler a bit complex). how does @serverless/vip feel about that?

This would be great. It removes the requirement for me to document how the integration templates work and allow our devs to set things like headers the way they do now in other frameworks.

It would be good to look at providing a serverless lambda proxy npm package that allows people to do things like setting default headers, enabling cors and handling transformation so you don't always have to remember to call JSON.stringify. It would also could be smart enough to allow people to run the one function in both methods and turn off its extra smarts when running not in proxy mode.

For some extended use cases like the use of custom authorizers it should be checked first, if really all API context variables are also proxied to the lambda context object. Especially for that use case it is important to have the principalId value available in the lambda to check which principal was allowed to access the method. If it is not available, this use case would rely on the template mapping intregration.

Another thing is the error response mapping. Reading the AWS documentation I'm not quite sure if error responses triggered with context.fail() will also be proxied back to the caller.

👍 for simplicity. Imho it should be included in API Gateway from day one to avoid all request/response mapping burden.
I remember spending many days getting these right and was the hardest nut to crack in serverless 0.5.
If the same can be done with context.fail() I could not think of any use cases where I would need the 'advantages' of mappings.

I don't really think this is an issue imo. We can still have "response templates" (or whatever we can call them), but at the code level. Let's not forget that Serverless allows sharing code between functions. So if the logic responsible for "response templates" lives in the lib folder or something, it can be used in all functions. I'd personally prefer that over configuration any day.

But we as Serverless can't do that for users. They would have to do and manage it themselves, so we're pushing standardised functionality that we can make easier through the framework into each and every codebase of every service.

I think as a framework we should provide only the easiest way to do things (as long as they're not limited of course).

I generally agree with that statement, the main problem I see here though is that having things like your headers defined purely in code will be a pain in the ass for a lot of people. And most importantly why I want to keep both implementation around is basically I don't know if there are a lot of use cases that will simply not be possible with Serverless and APIG any more without supporting the full range of features.

By default it should imho be the proxy integration by AWS, but removing the whole functionality that AWS/APIG can support is imho risky because it might mean that we're locking people out of functionality they need (and are already accustomed to). One example would be that we simply can't support CORS at the moment any more, because while we can set the header on the OPTIONS request we can't set the header on the GET, ... requests because thats not possible any more (tried it, or at least it doesn't work the way it worked before).

@HyperBrain the context.fail is I think outdated and you can use the callback handed to you directly. At least thats what is shown here in the docs I just tried the following with a proxy:

// Your first function handler
module.exports.hello = (event, context, cb) => {
  cb("testmessage", {"statusCode": "500", "headers": {'Cache-Control': "max-age=120"}, body: JSON.stringify({message: 'Go Serverless v1.0! Your function executed successfully!', event })});
};

Which results in the following internal server error by AWS:

< HTTP/1.1 502 Bad Gateway
< Content-Type: application/json
< Content-Length: 36
< Connection: keep-alive
< Date: Wed, 21 Sep 2016 12:47:23 GMT
< x-amzn-RequestId: 8b0f7978-7ff9-11e6-b323-f7314765d628
< X-Cache: Error from cloudfront
< Via: 1.1 7b6339693d82ec593824b8c6ad776117.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: T4wPVu3zaLYbRxGk55qVgHwibD1TltxKti_3Ya4oXzGxe7bfT7UuBg==
<
* Connection #0 to host k9mfds1kjg.execute-api.us-west-2.amazonaws.com left intact
{"message": "Internal server error"}

So error handling might still be an issue.

@flomotlik What happens if you return the response object in the error param of the callback?

@andymac4182 same thing, 502 Bad Gateway.

Agree with @flomotlik while strongly empathizing w/ @eahefnawy. If this is default behavior, I think it's a great UX, without breaking back compat and blocking use-cases which may not be supported.

@ac360 yeah imho giving the ability to switch integrations would go a long way for people to set this up. And as we can only support CORS through the complex setup (at least as far as I know because setting the header directly on the integration wasn't possible) we have to go there for many use cases anyway unless people set those headers themselves.

So error handling might still be an issue.

I think the new proxy actually handles error codes much better with this functionality. Previously you had to pass back something in the error object to match on for a status code mapping in APIG.

This meant that our lambda invocations would have to count as an error in CW to get a mapping to an APIG response for an error code. This absolutely trashes the value of monitoring for lambda invocation errors.

The new model of cb(error, proxyPassObject) means that we can track actual lambda errors but still pass through useful status codes to APIG! If you pass cb(error) then something should be really broken and a 502 from APIG is expected as it would by synonymous to your server being so broken it can't reply. You would wrap monitoring around your invocation errors as well, because they are now valuable.

@shortjared This approach is fine as long as you say error => FAIL everything else => OK (200).
But how would you map different error codes that are expected by the client to different HTTP error codes (maybe 4xx), Currently we have fully promisified code that resolves only if everything went ok, but emits different rejections for the various error states we support on our endpoints.
BTW, some endpoints even return 302 redirects.

The whole point of the proxy pass is that you can pass status codes as part of the success object.

    callback(null, {
        statusCode: 401,
        body: JSON.stringify(responseBody)
    });

Now your API returns the 401, but you don't have a thousand useless lambda invocation errors because some bozo is trying to enumerate resources on your API or something.

In the case of your promises, you would have to handle those at the top level in a handler or something, and then only if you have no understanding or handling of an error you'd want to call cb(error). Then you want your system to alert you because something you didn't expect has broken.

We too have a system that is all promises with rejections. Trying to figure out the best way to overhaul it right now. That to say, this is way more elegant. Because right now if someone abuses our API we get alerts and it turns out to be nothing.

Ok, that's a valid approach. As the proxy integration should be the first one to use in Serverless unless you need some functionality that dictates the use of the templates, Serverless could include some kind of error handler class in the default handler stub or a utility library that lets one easily map Error() instances to "error" success result objects. That would be a great addition.

Ok so from this discussion so far its clear we need to go with both for now imho and we can still remove the non-proxy one in the future if we want to. One change though I think we should change it from boolean to an enum, so instead of: proxy: true it will be integration: proxy (the default) and integration: SOMETHING_I_NEED_TO_COME_UP_WITH so that we can potentially add more in the future easily, e.g. mock, ...

We have hundreds of already-deployed functions that use request and response VTL templates. It's pretty clear that we'll need VTL functionality from AWS API Gateway also in the future to support all these projects.

I'm a proponent of loose integration, where API Gateway handles the protocol-specific adaptation (HTTP details) and Lambda executes a specific function without knowing anything about HTTP headers etc (input/output specified as JSON objects). I've always seen is as the "Amazon-sanctioned" approach. I don't know if they are planning to change the direction?

Serverless 1.0 default templates are not in too much contradiction with this approach, because they just supply lots of extra data to Lambda. It's just a good default that can be overridden.

I also believe that in the future, API Gateway will be used more and more without Lambda, by proxying requests directly to cloud service APIs and doing the necessary adaptation in VTL. Then you will be able to build services without code, just configuration. Force-pairing API Gateway always with Lambda is not good if this future happens.

agreed @kennu , looks like we've got a clear winner in making sure the different ways that can be used with APIG are used and we're not limiting to the proxy!

@flomotlik Is this one that the serverless team are implementing or is it open for other people to build?

@andymac4182 We've started working on it and will open a PR in the next hours.

Is that using CF or do you have to do it via the API @flomotlik ?

Cloudformation. It only requires config changes, but they didn't introduce any new CF resources for this. So implementation is pretty easy and straight forward.

Ahh cool. I contacted them but they said it wasn't supported in CF yet. 👍 We have one app that wasn't possible with the old way so can't wait for this.

@andymac4182 first PR: https://github.com/serverless/serverless/pull/2185/files

We'll keep iterating on it a bit, but basically here is the first iteration.

Excited to see this make it into serverless. I've just skimmed this issue and admittedly haven't had time to look at #2185 however just wanted to add a quick comment on CORS w/r/t APIG proxy.

IMO if a developer requires CORS, the sls default should be to let APIG handle the options request via mock integration request. I think this is best practice for a default as it is a performance improvement, cost savings and less code that has to be written/maintained.

The swagger is super simple, and can be defined just once and referenced in all the options path item objects via $ref (I know there was a bug with the APIG importer with this functionality, not sure if it has been fixed yet).

Here is an example:

---
swagger: "2.0"
info:
  version: "2016-09-22T21:51:08Z"
  title: "ryan-proxy-test"
host: "myuid.execute-api.us-east-1.amazonaws.com"
basePath: "/yrdy"
schemes:
- "https"
paths:
  /{proxy+}:
    options:
      consumes:
      - "application/json"
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
          headers:
            Access-Control-Allow-Origin:
              type: "string"
            Access-Control-Allow-Methods:
              type: "string"
            Access-Control-Allow-Headers:
              type: "string"
      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
        requestTemplates:
          application/json: "{\"statusCode\": 200}"
        passthroughBehavior: "when_no_match"
        type: "mock"
    x-amazon-apigateway-any-method:
      produces:
      - "application/json"
      parameters:
      - name: "proxy"
        in: "path"
        required: true
        type: "string"
      responses: {}
      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: "200"
        uri: "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:myAWSAccount:function:helloworld-proxy/invocations"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        cacheNamespace: "1rnijn"
        cacheKeyParameters:
        - "method.request.path.proxy"
        type: "aws_proxy"
definitions:
  Empty:
    type: "object"
    title: "Empty Schema"

IMO if a developer requires CORS, the sls default should be to let APIG handle the options request via mock integration request. I think this is best practice for a default as it is a performance improvement, cost savings and less code that has to be written/maintained

@doapp-ryanp This is already how we do it, we create an options request with a mock return

#2185 implements that /cc @flomotlik

It would still be nice if serverless would be able to 'forward' all calls to an existing spawned application as described in https://medium.com/@tjholowaychuk/blueprints-for-up-1-5f8197179275#.hgs6npci1. Of course this is not the most performant way, but UX wise this would be clever to people to just expose their express (or another) application through the AWS Lambda without thinking too much.