Skip to content

Commit

Permalink
add So long API Gateway, and thanks for all the routes draft
Browse files Browse the repository at this point in the history
  • Loading branch information
brettstack committed Sep 18, 2024
1 parent cedd6b9 commit 8fc8dee
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: 'So long API Gateway, and thanks for all the routes'

date: 2024-09-20
excerpt: |
For nearly a decade I've been building APIs with API Gateway and Lambda. However, most of the time I just need a way to invoke a Lambda Function over HTTP with a custom domain name, and this is exactly what CloudFront + Lambda Function URL (CLFURL) enables.
authors:
- brett
tags:
- api-gateway
- lambda
- aws
- cloudfront
- serverless
draft: true
---
import { Image } from 'astro:assets';
import clefairy from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/clefairy.gif'
import lfurlWorkaroundTweets from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/lfurl-workaround-tweets.png'
import cognitoJwtVerifier from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/cognito-jwt-verifier.png'
import jwtExpressMiddleware from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/jwt-express-middleware.png'
import apigwLfurlClfurlPerformance from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/apigw-lfurl-clfurl-performance.png'
import codeGenieMaxDescription from '../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/code-genie-max-description.png'

For nearly a decade I've been building APIs with API Gateway and Lambda. I was even fortunate enough to be part of the team that launched API Gateway back in 2015! It's served me well and I'm sure I'll use it again on future projects that require some of its more advanced features.

However, most of the time I just need a way to invoke a Lambda Function over HTTP with a custom domain name, and this is exactly what CloudFront + Lambda Function URL (CLFURL) enables.

<figure>
<img src={clefairy.src} alt="Clefairy" loading="eager" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
Clefairy: The unofficial mascot of CLFURL
</figcaption>
</figure>

## Why change?

### Cost
Since launch, one of the biggest criticisms of API Gateway has been its pricing. One would expect that the cost of Lambda (the thing that's doing the heavy lifting) would make up the larger portion of the cost pie compared to the thing that's simply connecting it to a client. In reality, API Gateway costs often exceed Lambda by more than 2x. Under similar conditions, CloudFront costs ~10% of Lambda costs.

:::caution[WARNING: Gross oversimplification to prove a point detected]
Of course saying API Gateway is simply a service that connects Lambda over HTTP is unfair. <a href="https://x.com/theburningmonk" target="_blank" rel="noopener">Yan Cui</a> has an excellent comparison of the two approaches in his blog post <a href="https://theburningmonk.com/2024/03/when-to-use-api-gateway-vs-lambda-function-urls/" target="_blank" rel="noopener">When to use API Gateway vs. Lambda Function URLs
</a>. He covers many of the advanced features of API Gateway and concludes with a preference for API Gateway in most scenarios
:::

The one area where API Gateway has a cost advantage is on unauthorised requests. When using the JWT Authorizer, API Gateway eats those costs for you. With CLFURL you would pay both the CloudFront and Lambda costs for unauthorized requests. This is really only a concern when it comes to DDOS (or Denial of Wallet) attacks. Fortunately, <a href="https://docs.aws.amazon.com/waf/latest/developerguide/ddos-event-mitigation-logic-continuous-inspection.html" target="_blank" rel="noopener">AWS Shield Standard</a>] (a free DDOS protection service included with CloudFront) *should* mitigate those attacks.


### Max timeout

API Gateway has a max timeout of 29 seconds, which is more than enough for most REST API needs. However, with AI becoming a core feature of many products, those extra 31 seconds afforded by CloudFront (a total of 1 minute) can be crucial.

In fact, this is the main reason I started investigating this option in the first place! <a href="https://codegenie.codes" target="_blank" rel="noopener">Code Genie</a> uses AI to generate data models based on the description of your app. There is currently a 500 character limit on this description to mitigate the chance of it taking longer than 29 seconds. By moving to CLFURL, Code Genie will be able to handle significantly more complex requests, and enable other AI features.

<figure>
<Image src={codeGenieMaxDescription} alt="500 characters is not enough to describe complex applications" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
500 characters is not enough to describe complex applications 😞
</figcaption>
</figure>

:::note[API Gateway max timeout can be increased on request]
API Gateway <a href="https://aws.amazon.com/about-aws/whats-new/2024/06/amazon-api-gateway-integration-timeout-limit-29-seconds/" target="_blank" rel="noopener">recently announced</a> it can increase the max timeout via support request. It's unknown if there are additional costs associated with this.
:::

### Performance?

In my tests comparing the performance of API Gateway to CloudFront, I found both approaches yield similar results. Here are the highlights:

1. CloudFront adds a shocking +300ms latency for cross-region (other-side-of-the-world) requests when not under load. These findings are why I didn't switch to CLFURL earlier this year. I recently decided to test the performance again, hoping that AWS had resolved the issue. They hadn't. However, after diving deeper I discovered that this latency is akin to a cold start. When under load, this extra latency is a rare occurrence.
2. Under load, CloudFront offers marginally better performance than API Gateway. ~4% faster on average.

<figure>
<Image src={apigwLfurlClfurlPerformance} alt="API Gateway vs Lambda Function URL vs CloudFront + LFURL" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
API Gateway vs Lambda Function URL vs CloudFront + LFURL
</figcaption>
</figure>

## Locking down the Lambda FURL

If you want to enforce CloudFront security features such as Shield and WAF, you need to ensure attackers can't bypass CloudFront by calling your Lambda Function URL directly. To accomplish this, <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html" target="_blank" rel="noopener">AWS recommends</a> setting the LFURL's authorization to AWS_IAM and using OAC to grant access for the distribution. If only it were that simple. Unfortunately, there are two big limitations:

1. CloudFront overrides the Authorization header when invoking your Lambda Function, so you need to use a different, non-standard header to include your JWT.
2. The client needs to sign the payload in PUT/POST methods. See this post by <a href="https://speedrun.nobackspacecrew.com/blog/2024/05/22/using-cloudfront-as-a-lightweight-proxy.html#lambda-oac" target="_blank" rel="noopener">David Behroozi</a> for more details.

A simpler solution proposed by <a href="https://x.com/ryan_sb" target="_blank" rel="noopener">Ryan Scott Brown</a> is to have CloudFront add a custom origin header with a secret: `'X-CloudFront-Secret': 'NoBadGuysAllowed'`. Your Lambda Function can check the secret in the header, and return an error code if it doesn't match. Another solution proposed by <a href="https://x.com/dreamorosi" target="_blank" rel="noopener">Andrea Amorosi</a> uses Lambda@Edge.

<a href="https://x.com/ryan_sb/status/1835363459979960624" target="_blank" rel="noopener">
<figure>
<Image src={lfurlWorkaroundTweets} alt="Tweets with alternative solutions for locking down Lambda Function URLs" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
Simpler solutions to locking down your Lambda FURL
</figcaption>
</figure>
</a>

## Replacing API Gateway Authorizer with in-Lambda JWT validation

The only "advanced" API Gateway feature I use on all of my APIs is the Cognito/JWT Authorizer. Since CloudFront doesn't have a similar native feature, we need to perform the JWT validation inside the Lambda Function. This has the added bonus of making our app more portable and easier to run locally, while also granting us more flexibility.

Performing JWT validation yourself is also incredibly fast (~4ms cold; ~0.3ms warm in Node.js) thanks to <a href="https://x.com/AWSbrett/status/1779422735539847454" target="_blank" rel="noopener">this tip by David Behroozi</a>.

<figure>
<Image src={jwtExpressMiddleware} alt="Express Middleware for JWT validation" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
Express Middleware for JWT validation
</figcaption>
</figure>

<figure>
<Image src={cognitoJwtVerifier} alt="Cognito JWT Validation via aws-jwt-verify" style={{margin: 'auto'}} />
<figcaption style={{ margin: 0, color: '#666', fontSize: '0.8rem', textAlign: 'center' }}>
Cognito JWT Validation via aws-jwt-verify
</figcaption>
</figure>


## Conclusion

CloudFront + Lambda Function URL (CLFURL) is an excellent combination for Serverless APIs when you don't need any of the advanced features offered by API Gateway. With CloudFront being an order of magnitude cheaper than API Gateway, it puts it more in line with what'd you'd expect when compared to the cost of other components of your Serverless architecture (and without sacrificing performance).

0 comments on commit 8fc8dee

Please sign in to comment.