Skip to content

Commit c3f046e

Browse files
committed
back to api gateway to avoid limited vercel function invocations
1 parent 515100d commit c3f046e

File tree

11 files changed

+1836
-1671
lines changed

11 files changed

+1836
-1671
lines changed

lambda/package-lock.json

+258-104
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lambda/src/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ require('dotenv').config()
44

55
export const supportedLanguages: SupportedLanguages[] = ['node', 'python', 'golang']
66

7-
export const DEFAULT_TIMEOUT_MS = 30000
7+
export const DEFAULT_TIMEOUT_MS = 28000
88
export const DEFAULT_MAX_RECURSIVE_CALLS = 256
99
export const DEFAULT_TMP_DIR_PATH = '/tmp'
1010
export const DEFAULT_TMP_FILE_MAX_SIZE_BYTES = 384e6 // because the size of ephemeral storage (/tmp folder) configured on AWS Lambda function is 512*10^6 bytes

lambda/src/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { validateAPIGatewayProxyEvent } from './validations/event'
77
const log = debug('app:handler')
88

99
export const handler: APIGatewayProxyHandler = async (event) => {
10+
// handle the preflight request (OPTIONS method withot req body) automatically sent by the browser before making a CORS request (e.g., POST)
11+
if (event.httpMethod === 'OPTIONS') return ok({})
12+
1013
const validatedEvent = validateAPIGatewayProxyEvent(event)
1114
if (validatedEvent.isError()) return badRequest(validatedEvent.value)
1215

@@ -34,4 +37,9 @@ const internalServerError = (reason: string) => result(500, { reason })
3437
const result = (statusCode: number, body: Record<string, any>) => ({
3538
statusCode,
3639
body: safeStringify(body),
40+
headers: {
41+
"Access-Control-Allow-Origin": "*",
42+
"Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE",
43+
"Access-Control-Allow-Headers": "Content-Type",
44+
},
3745
})

terraform/main.tf

+78-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ resource "aws_ecr_lifecycle_policy" "this" {
5252
})
5353
}
5454

55-
# if any file within lambda/** changes, run docker build locally and push a new image with latest tag to ECR repository
55+
# run docker build locally and push a new image with latest tag to ECR repository (if any file within lambda/** changed)
5656

5757
locals {
5858
lambda_path = "${path.module}/../lambda"
@@ -70,11 +70,13 @@ resource "null_resource" "local_docker_build_tag_push" {
7070
provisioner "local-exec" {
7171
working_dir = local.lambda_path
7272
command = <<EOT
73+
orb
7374
aws ecr get-login-password --profile ${var.profile} --region ${var.region} | docker login --username AWS --password-stdin ${aws_ecr_repository.this.repository_url}
7475
7576
docker build --platform linux/arm64 -t ${var.image_name} .
7677
docker tag ${var.image_name}:latest ${aws_ecr_repository.this.repository_url}:latest
7778
docker push ${aws_ecr_repository.this.repository_url}:latest
79+
orb stop
7880
EOT
7981
}
8082
}
@@ -86,21 +88,21 @@ data "aws_ecr_image" "latest" {
8688
image_tag = "latest"
8789
}
8890

89-
# lambda
91+
# lambda - create or force an update if a new image was pushed (image digest of latest tag changed)
9092

9193
resource "aws_lambda_function" "this" {
9294
depends_on = [data.aws_ecr_image.latest, aws_iam_role.lambda_execution]
9395

9496
function_name = var.function_name
9597
package_type = "Image"
96-
image_uri = "${aws_ecr_repository.this.repository_url}@${data.aws_ecr_image.latest.image_digest}" # include the current image digest of latest tag to force lambda update if a new image was pushed
98+
image_uri = "${aws_ecr_repository.this.repository_url}@${data.aws_ecr_image.latest.image_digest}"
9799
role = aws_iam_role.lambda_execution.arn
98100
timeout = 45
99101
memory_size = 512
100102
}
101103

104+
# lambda - iam role to be assumed
102105

103-
# iam role that the lambda will assume
104106
resource "aws_iam_role" "lambda_execution" {
105107
name = "${var.function_name}_lambda_execution_role"
106108

@@ -143,3 +145,75 @@ resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
143145
role = aws_iam_role.lambda_execution.name
144146
policy_arn = aws_iam_policy.lambda_policy.arn
145147
}
148+
149+
# api gateway
150+
151+
resource "aws_api_gateway_rest_api" "api" {
152+
name = var.api_name
153+
}
154+
155+
resource "aws_api_gateway_resource" "proxy" {
156+
rest_api_id = aws_api_gateway_rest_api.api.id
157+
parent_id = aws_api_gateway_rest_api.api.root_resource_id
158+
path_part = "api"
159+
}
160+
161+
resource "aws_api_gateway_method" "proxy_method" {
162+
rest_api_id = aws_api_gateway_rest_api.api.id
163+
resource_id = aws_api_gateway_resource.proxy.id
164+
http_method = "ANY"
165+
authorization = "NONE"
166+
}
167+
168+
resource "aws_api_gateway_integration" "lambda" {
169+
rest_api_id = aws_api_gateway_rest_api.api.id
170+
resource_id = aws_api_gateway_resource.proxy.id
171+
http_method = aws_api_gateway_method.proxy_method.http_method
172+
integration_http_method = "ANY"
173+
type = "AWS_PROXY"
174+
uri = aws_lambda_function.this.invoke_arn
175+
timeout_milliseconds = 29000
176+
}
177+
178+
resource "aws_api_gateway_method_response" "cors_response" {
179+
rest_api_id = aws_api_gateway_rest_api.api.id
180+
resource_id = aws_api_gateway_resource.proxy.id
181+
http_method = aws_api_gateway_method.proxy_method.http_method
182+
status_code = "200"
183+
184+
response_parameters = {
185+
"method.response.header.Access-Control-Allow-Origin" = true
186+
"method.response.header.Access-Control-Allow-Methods" = true
187+
"method.response.header.Access-Control-Allow-Headers" = true
188+
}
189+
}
190+
191+
resource "aws_api_gateway_integration_response" "cors_integration_response" {
192+
rest_api_id = aws_api_gateway_rest_api.api.id
193+
resource_id = aws_api_gateway_resource.proxy.id
194+
http_method = aws_api_gateway_method.proxy_method.http_method
195+
status_code = aws_api_gateway_method_response.cors_response.status_code
196+
197+
response_parameters = {
198+
"method.response.header.Access-Control-Allow-Origin" = "'*'"
199+
"method.response.header.Access-Control-Allow-Methods" = "'OPTIONS,GET,POST,PUT,DELETE'"
200+
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type'"
201+
}
202+
203+
depends_on = [aws_api_gateway_integration.lambda]
204+
}
205+
206+
resource "aws_api_gateway_deployment" "api_deployment" {
207+
depends_on = [aws_api_gateway_integration.lambda]
208+
rest_api_id = aws_api_gateway_rest_api.api.id
209+
stage_name = "production"
210+
}
211+
212+
resource "aws_lambda_permission" "apigw_lambda" {
213+
statement_id = "AllowAPIGatewayInvoke"
214+
action = "lambda:InvokeFunction"
215+
function_name = aws_lambda_function.this.function_name
216+
principal = "apigateway.amazonaws.com"
217+
218+
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
219+
}

terraform/outputs.tf

+4
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ output "ecr_repository_url" {
55
output "lambda_function_arn" {
66
value = aws_lambda_function.this.arn
77
}
8+
9+
output "api_gw_url" {
10+
value = aws_api_gateway_deployment.api_deployment.invoke_url
11+
}

terraform/variables.tf

+4
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ variable "image_name" {
2828
variable "function_name" {
2929
type = string
3030
}
31+
32+
variable "api_name" {
33+
type = string
34+
}

web/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"typesync": "npx typesync"
1919
},
2020
"dependencies": {
21-
"@aws-sdk/client-lambda": "^3.688.0",
2221
"@vercel/speed-insights": "^1.0.12",
2322
"next": "^14.2.15",
2423
"prism-react-renderer": "^2.4.0",

web/src/action/run-function.ts

-70
This file was deleted.

web/src/api.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Either, error, success } from './lib/either'
2+
import { safeParse, safeStringify } from './static/safe-json'
3+
import { FunctionData, Language, TreeViewerData } from './types'
4+
5+
type RequestBody = {
6+
lang: Language
7+
functionData: FunctionData
8+
options: {
9+
memoize: boolean
10+
}
11+
}
12+
13+
export const runFunction = async (
14+
requestBody: RequestBody
15+
): Promise<Either<string, TreeViewerData>> => {
16+
try {
17+
const response = await fetch('https://c1y17h6s33.execute-api.us-east-1.amazonaws.com/production/run', {
18+
method: 'POST',
19+
headers: {
20+
'Content-Type': 'application/json',
21+
},
22+
mode: 'cors',
23+
body: safeStringify(requestBody),
24+
})
25+
const responseBody = await response.text()
26+
27+
if (response.ok) {
28+
const treeViewerData = safeParse(responseBody) as TreeViewerData
29+
return success(treeViewerData)
30+
} else {
31+
const err = safeParse(responseBody) as { reason: string }
32+
return error(err.reason || 'Internal server error')
33+
}
34+
} catch (e) {
35+
console.error(e)
36+
return error('Unexpected error')
37+
}
38+
}

web/src/components/app/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use client'
2-
import React, { useState } from 'react'
2+
import { useState } from 'react'
33
import { toast } from 'react-hot-toast'
44
import styled from 'styled-components'
5-
import runFunction from '../../action/run-function'
65
import { ThemeName } from '../../styles/themes'
76
import { FunctionData, Language, TreeViewerData } from '../../types'
87
import FunctionForm from '../function-form'
98
import GraphViewer from '../graph-viewer'
109
import Footer from './footer'
10+
import { runFunction } from '../../api'
1111

1212
const App = (props: { onThemeChange: (themeName: ThemeName) => void }) => {
1313
const [treeViewerData, setTreeViewerData] = useState<TreeViewerData>(null)
@@ -28,7 +28,7 @@ const App = (props: { onThemeChange: (themeName: ThemeName) => void }) => {
2828
options: { memoize: options.memoize },
2929
})
3030

31-
if (result.ok) {
31+
if (result.isSuccess()) {
3232
setTreeViewerData(result.value)
3333
setTreeViewerOptions({ animate: options.animate })
3434
} else {

0 commit comments

Comments
 (0)