Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add aws-lambda-edge preset with CDK #240

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bce1771
feat: add aws-lambda-edge preset
WinterYukky May 14, 2022
c87fde6
docs(aws-lambda-edge): add documentation
WinterYukky May 15, 2022
69049af
docs(aws-lambda-edge): fix broken alert
WinterYukky May 15, 2022
81759fc
feat(prest): add deployment code generater
WinterYukky Jul 7, 2022
7e5deb1
fix(preset): fix empty public dir
WinterYukky Jul 7, 2022
12b179a
docs(preset): update to Zero Config Provider
WinterYukky Jul 7, 2022
8dd657a
docs(preset): do not use REGION env
WinterYukky Jul 7, 2022
a7a7af1
fix(preset): add @types/node
WinterYukky Jul 7, 2022
d0264ab
docs(preset): add aws to Zero-Config Providers
WinterYukky Jul 7, 2022
6024e2e
docs(preset): fix message title
WinterYukky Jul 7, 2022
97a89fe
feat(preset): nitro-asset to npm package
WinterYukky Jul 9, 2022
f174d66
docs(preset): update customization example
WinterYukky Jul 9, 2022
ab707c9
docs(preset): update deploy page composition
WinterYukky Jul 9, 2022
79fd8e4
feat(preset): Easier to set specific region
WinterYukky Jul 11, 2022
ba5f8fc
docs(preset): add all option to deploy command
WinterYukky Jul 11, 2022
fd38fab
chore(preset): remove mkdir process
WinterYukky Jul 11, 2022
9684073
docs(preset): update about bootstrap
WinterYukky Jul 11, 2022
53c185a
docs(preset): add new line to bootstrap command
WinterYukky Jul 11, 2022
2ac76e7
chore(preset): remove bootstrap command
WinterYukky Jul 11, 2022
9b8d39a
Merge branch 'main' of https://github.com/WinterYukky/nitro into feat…
WinterYukky Nov 1, 2022
a62fa09
chore(preset): add general properties
WinterYukky Nov 1, 2022
c51917c
docs(preset): update to docus syntax
WinterYukky Nov 1, 2022
08a7340
fix(docs): warning to alert
WinterYukky Nov 1, 2022
ce492b7
docs(preset): set warning type to alert
WinterYukky Nov 1, 2022
af349c0
docs(preset): fix warning position
WinterYukky Nov 1, 2022
a5245ed
Merge branch 'main' into pr/WinterYukky/240
pi0 May 17, 2023
761680c
[autofix.ci] apply automated fixes
autofix-ci[bot] May 17, 2023
3b2d3e7
fix: append query string
pi0 May 17, 2023
35139d6
doc: fix test comment
WinterYukky Nov 4, 2023
4fae105
build: bump to NODEJS_18_X
WinterYukky Nov 4, 2023
f93e91c
fix: remove externals property
WinterYukky Nov 4, 2023
cf7e69f
fix: unnormalized body
WinterYukky Nov 4, 2023
7b387af
feat: support dynamic path resolve
WinterYukky Nov 4, 2023
84555a2
build: use jiti
WinterYukky Nov 4, 2023
52cf689
Merge branch 'main' of https://github.com/WinterYukky/nitro into feat…
WinterYukky Nov 4, 2023
9ea6ed7
chore: apply automated fixes
autofix-ci[bot] Nov 4, 2023
144d35a
test: fix to pass the new tests
WinterYukky Nov 4, 2023
08a5111
doc: add AWS Lambda@Edge
WinterYukky Nov 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/deploy/providers/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,89 @@ import { handler } from './.output/server'
// Use programmatically
const { statusCode, headers, body } = handler({ rawPath: '/' })
```

# AWS Lambda@Edge

**Preset:** `aws-lambda-edge` ([switch to this preset](/deploy/#changing-the-deployment-preset))

Nitro provides a built-in preset to generate output format compatible with [AWS Lambda@Edge](https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html).

The output entrypoint in `.output/server/index.mjs` is compatible with [AWS Lambda@Edge format](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html).

### Deploy using AWS CDK

The following code is an example of deploying a Nuxt3 project to CloudFront and Lambda@Edge with [AWS CDK](https://github.com/aws/aws-cdk). Using this stack, paths under `_nuxt/` (static assets) will get their data from the S3 origin, and all other paths will be resolved by Lambda@Edge.

```ts
import { spawnSync } from "child_process";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can auto generate this script to the .output. avoiding to hardcode things like public path to the example.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the app build part? I'd like that. Maybe the output could be an object containing the key locations and configs that can be used for cloud specific deployments (for example configuring the S3 bucket and cloudfront caching):

{
  "serverHandler": ".output/server",
  "assets": ".output/assets"
  "public": ".output/public"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review.
This script is not a simple script, it is IaC, so it may be difficult to put it in the .output from a DX perspective.
I think a developer wants more freedom to customize it.

Is the key here that the developer needs to be aware of the public and server paths?

Copy link
Member

@pi0 pi0 Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely developers can always override to customize. We can export smaller utils even for better flexibility.

Is the key here that the developer needs to be aware of the public and server paths?

Yes. Such things shouldn't be hardcoded into the repository code or docs but auto-generated even considering customization needs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been considering various use cases and interfaces for the past week, but I think I've been overthinking things a bit.
As @chris-visser mentioned, it might be better to just include the serverDir and publicDir in nitro.json.
What do you think of that idea, @pi0?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand. These settings are easily extracted from the nuxt.config.ts since they are either Nuxt's defaults or set in that config. So maybe its not even needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I can agree your thinking @chris-visser. However to find configuration like nuxt.config.ts or nitro.config.ts and more... is a bit difficult for CDK apps.
Also maybe understood @pi0's thinking. His goal would be a Zero-Config Providers. Certainly that is one of the important features so I will try implemantation for auto generate this script to the .output.

import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";

export class NitroLambdaEdgeStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

spawnSync("yarn", ["build"], {
stdio: "inherit",
cwd: "<your-project-path>",
});

const edgeFunction = new cloudfront.experimental.EdgeFunction(
this,
"EdgeFunction",
{
runtime: lambda.Runtime.NODEJS_16_X,
handler: "index.handler",
code: lambda.Code.fromAsset("<your-project-path>/.output/server"),
}
);
const bucket = new s3.Bucket(this, "Bucket", {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
const s3Origin = new origins.S3Origin(bucket);
const distribution = new cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin: s3Origin,
edgeLambdas: [
{
functionVersion: edgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
},
],
},
additionalBehaviors: {
"_nuxt/*": {
origin: s3Origin,
},
},
});
new s3deployment.BucketDeployment(this, "Deployment", {
sources: [s3deployment.Source.asset("<your-project-path>/.output/public")],
destinationBucket: bucket,
distribution,
});
new CfnOutput(this, "URL", {
value: `https://${distribution.distributionDomainName}`,
});
}
}
```

::: warning Specify Region
Note that the region must be specified when using the code above.
:::

```ts
const app = new cdk.App();
new NitroLambdaEdgeStack(app, "NitroLambdaEdgeStack", {
env: {
region: "your AWS region", // need this line
},
});
```
6 changes: 6 additions & 0 deletions src/presets/aws-lambda-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineNitroPreset } from '../preset'

export const awsLambdaEdge = defineNitroPreset({
entry: '#internal/nitro/entries/aws-lambda-edge',
externals: true
})
1 change: 1 addition & 0 deletions src/presets/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './aws-lambda'
export * from './aws-lambda-edge'
export * from './azure-functions'
export * from './azure'
export * from './base-worker'
Expand Down
31 changes: 31 additions & 0 deletions src/runtime/entries/aws-lambda-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CloudFrontHeaders, Context, CloudFrontRequestEvent, CloudFrontResultResponse } from 'aws-lambda'
import '#internal/nitro/virtual/polyfill'
import { nitroApp } from '../app'

export const handler = async function handler (event: CloudFrontRequestEvent, context: Context): Promise<CloudFrontResultResponse> {
const request = event.Records[0].cf.request

const r = await nitroApp.localCall({
event,
url: request.uri,
context,
headers: normalizeIncomingHeaders(request.headers),
method: request.method,
query: request.querystring,
body: request.body
})

return {
status: r.status.toString(),
headers: normalizeOutgoingHeaders(r.headers),
body: r.body.toString()
}
}

function normalizeIncomingHeaders (headers: CloudFrontHeaders) {
return Object.fromEntries(Object.entries(headers).map(([key, keyValues]) => [key, keyValues.map(kv => kv.value)]))
}

function normalizeOutgoingHeaders (headers: Record<string, string | string[] | undefined>): CloudFrontHeaders {
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, Array.isArray(v) ? v.map(value => ({ value })) : [{ value: v }]]))
}
50 changes: 50 additions & 0 deletions test/presets/aws-lambda-edge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { resolve } from 'pathe'
import { describe } from 'vitest'
import destr from 'destr'
import type { CloudFrontHeaders, CloudFrontRequestEvent, CloudFrontResultResponse } from 'aws-lambda'
import { setupTest, testNitro } from '../tests'

describe('nitro:preset:aws-lambda-edge', async () => {
const ctx = await setupTest('aws-lambda-edge')
testNitro(ctx, async () => {
const { handler } = await import(resolve(ctx.outDir, 'server/index.mjs'))
return async ({ url: rawRelativeUrl, headers, method, body }) => {
// creating new URL object to parse query easier
const url = new URL(`https://example.com${rawRelativeUrl}`)
// modify headers to CloudFrontHeaders.
const reqHeaders: CloudFrontHeaders = Object.fromEntries(Object.entries(headers || {}).map(([k, v]) => [k, Array.isArray(v) ? v.map(value => ({ value })) : [{ value: v }]]))

const event: CloudFrontRequestEvent = {
Records: [
{
cf: {
config: {
distributionDomainName: 'nitro.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'origin-request',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ=='
},
request: {
clientIp: '203.0.113.178',
method: method || 'GET',
uri: url.pathname,
querystring: url.searchParams.toString(),
headers: reqHeaders,
body
}
}
}
]
}
const res: CloudFrontResultResponse = await handler(event)
// responsed CloudFrontHeaders are special, so modify them for testing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

const resHeaders = Object.fromEntries(Object.entries(res.headers).map(([key, keyValues]) => [key, keyValues.map(kv => kv.value).join(',')]))

return {
data: destr(res.body),
status: parseInt(res.status),
headers: resHeaders
}
}
})
})