@thunder-so/thunder 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.kiro/settings/lsp.json +198 -0
- package/README.md +60 -118
- package/docs/fargate-basic.md +222 -0
- package/docs/fargate-full.md +177 -0
- package/docs/fargate-nixpacks.md +199 -0
- package/docs/frameworks/analogjs-fargate.md +115 -0
- package/docs/frameworks/analogjs-serverless.md +121 -0
- package/docs/frameworks/astro-fargate.md +120 -0
- package/docs/frameworks/astro-serverless.md +112 -0
- package/docs/frameworks/astro-static.md +108 -0
- package/docs/frameworks/nextjs-fargate-dockerfile.md +227 -0
- package/docs/frameworks/nextjs-fargate-nixpacks.md +113 -0
- package/docs/frameworks/nextjs-static.md +160 -0
- package/docs/frameworks/nuxt-fargate.md +115 -0
- package/docs/frameworks/nuxt-serverless.md +122 -0
- package/docs/frameworks/solidstart-fargate.md +115 -0
- package/docs/frameworks/solidstart-serverless.md +116 -0
- package/docs/frameworks/sveltekit-fargate.md +130 -0
- package/docs/frameworks/sveltekit-serverless.md +120 -0
- package/docs/frameworks/tanstack-start-fargate.md +115 -0
- package/docs/frameworks/tanstack-start-serverless.md +205 -0
- package/docs/lambda-basic.md +178 -0
- package/docs/lambda-containers.md +179 -0
- package/docs/lambda-full.md +185 -0
- package/docs/serverless.md +267 -0
- package/docs/static-basic.md +151 -0
- package/docs/static-edge-functions.md +187 -0
- package/docs/static-full.md +145 -0
- package/index.ts +8 -8
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Deploy Full-Stack Meta-Frameworks to AWS Lambda + S3 + CloudFront
|
|
2
|
+
|
|
3
|
+
Thunder's `Serverless` construct deploys modern full-stack frameworks with server-side rendering (SSR) to AWS using a hybrid architecture: [AWS Lambda](https://aws.amazon.com/lambda/) handles dynamic server requests, [S3](https://aws.amazon.com/s3/) hosts static assets, and [CloudFront](https://aws.amazon.com/cloudfront/) unifies both behind a single domain with intelligent routing.
|
|
4
|
+
|
|
5
|
+
This pattern works with any meta-framework that uses [Nitro](https://nitro.unjs.io/) or a compatible server runtime: Nuxt, Astro, TanStack Start, SvelteKit, Solid Start, AnalogJS, and more.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
User Request
|
|
11
|
+
↓
|
|
12
|
+
CloudFront (CDN)
|
|
13
|
+
├─→ /assets/* → S3 (static files: JS, CSS, images)
|
|
14
|
+
├─→ /api/* → Lambda (API routes)
|
|
15
|
+
└─→ /* → Lambda (SSR pages)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Static assets** (`*.js`, `*.css`, `*.png`, etc.) are cached long-term at CloudFront edge locations
|
|
19
|
+
- **Dynamic requests** (SSR pages, API routes) hit Lambda through API Gateway
|
|
20
|
+
- **Single domain** - no CORS issues, unified caching strategy
|
|
21
|
+
|
|
22
|
+
## AWS Resources
|
|
23
|
+
|
|
24
|
+
| Resource | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| [Lambda Function](https://aws.amazon.com/lambda/) | Runs your server-side code (SSR, API routes) |
|
|
27
|
+
| [API Gateway HTTP API](https://aws.amazon.com/api-gateway/) | Routes dynamic requests to Lambda |
|
|
28
|
+
| [S3 Bucket](https://aws.amazon.com/s3/) | Hosts static assets (JS, CSS, images) |
|
|
29
|
+
| [CloudFront Distribution](https://aws.amazon.com/cloudfront/) | Global CDN with origin routing |
|
|
30
|
+
| [Origin Access Control (OAC)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) | Secures S3 - no public bucket access |
|
|
31
|
+
| [ACM Certificate](https://aws.amazon.com/certificate-manager/) | SSL/TLS for custom domain (optional) |
|
|
32
|
+
| [Route53](https://aws.amazon.com/route53/) | DNS A + AAAA records (optional) |
|
|
33
|
+
|
|
34
|
+
## Supported Frameworks
|
|
35
|
+
|
|
36
|
+
| Framework | Construct | Server Runtime | Notes |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| [Nuxt](https://nuxt.com/) | `Nuxt` | Nitro | Vue-based, `aws-lambda` preset |
|
|
39
|
+
| [Astro](https://astro.build/) | `Astro` | @astro-aws/adapter | Requires Lambda@Edge fallback |
|
|
40
|
+
| [TanStack Start](https://tanstack.com/start) | `TanStackStart` | Nitro | React-based, explicit `aws-lambda` preset required |
|
|
41
|
+
| [SvelteKit](https://kit.svelte.dev/) | `SvelteKit` | @foladayo/sveltekit-adapter-lambda | Requires `serveStatic: true` |
|
|
42
|
+
| [Solid Start](https://start.solidjs.com/) | `SolidStart` | Nitro | SolidJS-based, `aws-lambda` preset |
|
|
43
|
+
| [AnalogJS](https://analogjs.org/) | `AnalogJS` | Nitro | Angular-based, `aws-lambda` preset |
|
|
44
|
+
|
|
45
|
+
See [framework-specific guides](#framework-guides) below for setup instructions.
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) configured
|
|
50
|
+
- [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) bootstrapped:
|
|
51
|
+
```bash
|
|
52
|
+
cdk bootstrap aws://YOUR_ACCOUNT_ID/us-east-1
|
|
53
|
+
```
|
|
54
|
+
- Your app built with framework-specific build command
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bun add @thunder-so/thunder --development
|
|
60
|
+
# or
|
|
61
|
+
npm install @thunder-so/thunder --save-dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Basic Example (Nuxt)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Cdk, Nuxt, type NuxtProps } from '@thunder-so/thunder';
|
|
68
|
+
|
|
69
|
+
const config: NuxtProps = {
|
|
70
|
+
env: { account: '123456789012', region: 'us-east-1' },
|
|
71
|
+
application: 'myapp',
|
|
72
|
+
service: 'web',
|
|
73
|
+
environment: 'prod',
|
|
74
|
+
rootDir: '.',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
new Nuxt(new Cdk.App(), 'myapp-web-prod-stack', config);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Deploy
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Build your app first
|
|
84
|
+
bun run build
|
|
85
|
+
|
|
86
|
+
# Deploy
|
|
87
|
+
npx cdk deploy --app "npx tsx stack/prod.ts" --profile default
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
CDK outputs the CloudFront URL and API Gateway URL:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Outputs:
|
|
94
|
+
myapp-web-prod-stack.CloudFrontUrl = https://d1234abcd.cloudfront.net
|
|
95
|
+
myapp-web-prod-stack.ApiGatewayUrl = https://abc123.execute-api.us-east-1.amazonaws.com
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Custom Domain (Optional)
|
|
99
|
+
|
|
100
|
+
1. [Create a Route53 Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/AboutHZWorkingWith.html)
|
|
101
|
+
2. [Request a global ACM certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) in **`us-east-1`** (for CloudFront)
|
|
102
|
+
3. [Request a regional ACM certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) in your **function's region** (for API Gateway)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const config: ServerlessBaseProps = {
|
|
106
|
+
// ...
|
|
107
|
+
domain: 'app.example.com',
|
|
108
|
+
hostedZoneId: 'Z1234567890ABC',
|
|
109
|
+
globalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abc-123',
|
|
110
|
+
regionalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/def-456',
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> CloudFront requires a certificate in `us-east-1` (global). API Gateway requires a certificate in the same region as your Lambda function.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
### Server (Lambda)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
serverProps: {
|
|
122
|
+
memorySize: 1792,
|
|
123
|
+
timeout: 10,
|
|
124
|
+
keepWarm: true,
|
|
125
|
+
tracing: true,
|
|
126
|
+
variables: [
|
|
127
|
+
{ NODE_ENV: 'production' },
|
|
128
|
+
],
|
|
129
|
+
secrets: [
|
|
130
|
+
{ key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/db-abc123' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### CloudFront Cache Behavior
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Cache control
|
|
139
|
+
allowHeaders: ['Accept-Language'],
|
|
140
|
+
allowCookies: ['session-*'],
|
|
141
|
+
allowQueryParams: ['lang'],
|
|
142
|
+
denyQueryParams: ['utm_source', 'fbclid'], // mutually exclusive with allowQueryParams
|
|
143
|
+
|
|
144
|
+
// Custom error page
|
|
145
|
+
errorPagePath: '/404.html',
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Container Mode (Docker)
|
|
149
|
+
|
|
150
|
+
For larger apps or custom runtimes, use Docker:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
serverProps: {
|
|
154
|
+
dockerFile: 'Dockerfile',
|
|
155
|
+
dockerBuildArgs: { NODE_ENV: 'production' },
|
|
156
|
+
memorySize: 2048,
|
|
157
|
+
},
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Thunder builds and pushes the image to [Amazon ECR](https://aws.amazon.com/ecr/) automatically. See framework-specific docs for Dockerfile examples.
|
|
161
|
+
|
|
162
|
+
## Environment Variables
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
serverProps: {
|
|
166
|
+
variables: [
|
|
167
|
+
{ NODE_ENV: 'production' },
|
|
168
|
+
{ API_BASE_URL: 'https://api.example.com' },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Secrets from AWS Secrets Manager
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
aws secretsmanager create-secret \
|
|
177
|
+
--name "/myapp/DATABASE_URL" \
|
|
178
|
+
--secret-string "postgres://user:pass@host/db"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
serverProps: {
|
|
183
|
+
secrets: [
|
|
184
|
+
{ key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123' },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Keep Warm
|
|
190
|
+
|
|
191
|
+
Prevent cold starts by pinging the Lambda every 5 minutes:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
serverProps: {
|
|
195
|
+
keepWarm: true,
|
|
196
|
+
},
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Stack Outputs
|
|
200
|
+
|
|
201
|
+
| Output | Description |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `CloudFrontUrl` | CloudFront distribution URL |
|
|
204
|
+
| `ApiGatewayUrl` | API Gateway endpoint URL |
|
|
205
|
+
| `Route53Domain` | Custom domain URL (only if domain is configured) |
|
|
206
|
+
|
|
207
|
+
## Framework Guides
|
|
208
|
+
|
|
209
|
+
Detailed setup instructions for each framework:
|
|
210
|
+
|
|
211
|
+
- [Nuxt Serverless](./frameworks/nuxt-serverless.md)
|
|
212
|
+
- [Astro Serverless](./frameworks/astro-serverless.md)
|
|
213
|
+
- [TanStack Start Serverless](./frameworks/tanstack-start-serverless.md)
|
|
214
|
+
- [SvelteKit Serverless](./frameworks/sveltekit-serverless.md)
|
|
215
|
+
- [Solid Start Serverless](./frameworks/solidstart-serverless.md)
|
|
216
|
+
- [AnalogJS Serverless](./frameworks/analogjs-serverless.md)
|
|
217
|
+
|
|
218
|
+
## Generic Serverless Deployment
|
|
219
|
+
|
|
220
|
+
For any Vite/Nitro-based meta-framework not explicitly supported, use the generic `Serverless` construct:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { Cdk, Serverless, type ServerlessProps } from '@thunder-so/thunder';
|
|
224
|
+
|
|
225
|
+
const config: ServerlessProps = {
|
|
226
|
+
env: { account: '123456789012', region: 'us-east-1' },
|
|
227
|
+
application: 'myapp',
|
|
228
|
+
service: 'web',
|
|
229
|
+
environment: 'prod',
|
|
230
|
+
rootDir: '.',
|
|
231
|
+
|
|
232
|
+
serverProps: {
|
|
233
|
+
codeDir: '.output/server', // Your framework's server output
|
|
234
|
+
handler: 'index.handler',
|
|
235
|
+
runtime: Cdk.aws_lambda.Runtime.NODEJS_22_X,
|
|
236
|
+
architecture: Cdk.aws_lambda.Architecture.ARM_64,
|
|
237
|
+
memorySize: 1792,
|
|
238
|
+
timeout: 10,
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
clientProps: {
|
|
242
|
+
outputDir: '.output/public', // Your framework's static assets
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
new Serverless(
|
|
247
|
+
new Cdk.App(),
|
|
248
|
+
'myapp-web-prod-stack',
|
|
249
|
+
{ ...config, framework: 'custom' }
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This works with any framework that outputs a Lambda-compatible handler and static assets.
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npx cdk destroy --app "npx tsx stack/prod.ts" --profile default
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
> The S3 bucket uses `RemovalPolicy.RETAIN` to prevent accidental data loss. Delete it manually from the AWS console if needed.
|
|
262
|
+
|
|
263
|
+
## Related
|
|
264
|
+
|
|
265
|
+
- [lambda-basic.md](./lambda-basic.md) - Lambda construct reference
|
|
266
|
+
- [static-basic.md](./static-basic.md) - Static construct reference
|
|
267
|
+
- [fargate-basic.md](./fargate-basic.md) - Fargate construct reference
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Deploy Static Sites and SPAs to AWS S3 + CloudFront
|
|
2
|
+
|
|
3
|
+
Host your frontend on AWS in minutes. Thunder's `Static` construct deploys your build output to [Amazon S3](https://aws.amazon.com/s3/) and serves it globally through a [CloudFront](https://aws.amazon.com/cloudfront/) CDN - with HTTPS, HTTP/3, Brotli compression, and security headers out of the box.
|
|
4
|
+
|
|
5
|
+
Works with any framework that produces a static output folder: Vite (React, Vue, Svelte, Solid), Next.js static export, Astro SSG, Gatsby, and more.
|
|
6
|
+
|
|
7
|
+
## AWS Resources
|
|
8
|
+
|
|
9
|
+
| Resource | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| [S3 Bucket](https://aws.amazon.com/s3/) | Stores your build output. Private, accessed only via CloudFront OAC. |
|
|
12
|
+
| [CloudFront Distribution](https://aws.amazon.com/cloudfront/) | Global CDN. HTTP/3, TLS 1.2+, Brotli/Gzip compression. |
|
|
13
|
+
| [Origin Access Control (OAC)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) | Secures S3 - no public bucket access. |
|
|
14
|
+
| [ACM Certificate](https://aws.amazon.com/certificate-manager/) | SSL/TLS for your custom domain (optional). |
|
|
15
|
+
| [Route53](https://aws.amazon.com/route53/) | DNS A + AAAA records for IPv4/IPv6 (optional). |
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) configured with credentials
|
|
20
|
+
- [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) bootstrapped in your target account/region:
|
|
21
|
+
```bash
|
|
22
|
+
cdk bootstrap aws://YOUR_ACCOUNT_ID/us-east-1
|
|
23
|
+
```
|
|
24
|
+
- Your app's build output directory (e.g. `dist/`)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun add @thunder-so/thunder --development
|
|
30
|
+
# or
|
|
31
|
+
npm install @thunder-so/thunder --save-dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Stack File
|
|
35
|
+
|
|
36
|
+
Create `stack/dev.ts` (use separate files per environment):
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Cdk, Static, type StaticProps } from '@thunder-so/thunder';
|
|
40
|
+
|
|
41
|
+
const config: StaticProps = {
|
|
42
|
+
env: {
|
|
43
|
+
account: '123456789012',
|
|
44
|
+
region: 'us-east-1',
|
|
45
|
+
},
|
|
46
|
+
application: 'myapp',
|
|
47
|
+
service: 'web',
|
|
48
|
+
environment: 'dev',
|
|
49
|
+
|
|
50
|
+
rootDir: '.', // monorepo: e.g. 'apps/web'
|
|
51
|
+
outputDir: 'dist', // your framework's build output folder
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
new Static(
|
|
55
|
+
new Cdk.App(),
|
|
56
|
+
`${config.application}-${config.service}-${config.environment}-stack`,
|
|
57
|
+
config
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Deploy
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx cdk deploy --app "npx tsx stack/dev.ts" --profile default
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After deployment, CDK outputs the CloudFront distribution URL:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Outputs:
|
|
71
|
+
myapp-web-dev-stack.DistributionUrl = https://d1234abcd.cloudfront.net
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Custom Domain (Optional)
|
|
75
|
+
|
|
76
|
+
To serve from your own domain you need:
|
|
77
|
+
|
|
78
|
+
1. A [Route53 Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/AboutHZWorkingWith.html) for your domain
|
|
79
|
+
2. A [public ACM certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) issued in **`us-east-1`** (required for CloudFront, regardless of your app's region)
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const config: StaticProps = {
|
|
83
|
+
// ...
|
|
84
|
+
domain: 'app.example.com',
|
|
85
|
+
globalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abc-123',
|
|
86
|
+
hostedZoneId: 'Z1234567890ABC',
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> The certificate **must** be in `us-east-1` because CloudFront is a global service.
|
|
91
|
+
|
|
92
|
+
## Configuration Reference
|
|
93
|
+
|
|
94
|
+
### Core
|
|
95
|
+
|
|
96
|
+
| Property | Type | Required | Description |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `env.account` | `string` | Yes | AWS account ID |
|
|
99
|
+
| `env.region` | `string` | Yes | AWS region |
|
|
100
|
+
| `application` | `string` | Yes | Project identifier |
|
|
101
|
+
| `service` | `string` | Yes | Service identifier |
|
|
102
|
+
| `environment` | `string` | Yes | Environment name (e.g. `prod`) |
|
|
103
|
+
| `rootDir` | `string` | No | Root of your app. Defaults to `.` |
|
|
104
|
+
| `outputDir` | `string` | No | Build output directory. Defaults to `dist` |
|
|
105
|
+
| `debug` | `boolean` | No | Enables S3 access logs and CloudFront logging |
|
|
106
|
+
|
|
107
|
+
### Domain
|
|
108
|
+
|
|
109
|
+
| Property | Type | Description |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `domain` | `string` | Custom domain, e.g. `app.example.com` |
|
|
112
|
+
| `globalCertificateArn` | `string` | ACM certificate ARN (must be `us-east-1`) |
|
|
113
|
+
| `hostedZoneId` | `string` | Route53 hosted zone ID |
|
|
114
|
+
|
|
115
|
+
### CloudFront Behavior
|
|
116
|
+
|
|
117
|
+
| Property | Type | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `errorPagePath` | `string` | Custom 404 page path, e.g. `/404.html`. Defaults to `/index.html` |
|
|
120
|
+
| `allowHeaders` | `string[]` | Headers to include in cache key and forward to origin |
|
|
121
|
+
| `allowCookies` | `string[]` | Cookies to include in cache key |
|
|
122
|
+
| `allowQueryParams` | `string[]` | Query params to include in cache key |
|
|
123
|
+
| `denyQueryParams` | `string[]` | Query params to strip from cache key (e.g. UTM params). Mutually exclusive with `allowQueryParams` |
|
|
124
|
+
|
|
125
|
+
## Default Security Headers
|
|
126
|
+
|
|
127
|
+
The distribution ships with a secure-by-default response headers policy:
|
|
128
|
+
|
|
129
|
+
| Header | Value |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` |
|
|
132
|
+
| `X-Frame-Options` | `DENY` |
|
|
133
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
134
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
135
|
+
| `Content-Security-Policy` | `default-src 'self'; style-src https: 'unsafe-inline'; ...` |
|
|
136
|
+
| `X-XSS-Protection` | `1; mode=block` |
|
|
137
|
+
|
|
138
|
+
To add custom headers per path, see [static-edge-functions.md](./static-edge-functions.md).
|
|
139
|
+
|
|
140
|
+
## Destroy
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx cdk destroy --app "npx tsx stack/dev.ts" --profile default
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> The S3 hosting bucket uses `RemovalPolicy.RETAIN` to prevent accidental data loss. Delete it manually from the AWS console if needed.
|
|
147
|
+
|
|
148
|
+
## Next Steps
|
|
149
|
+
|
|
150
|
+
- [static-edge-functions.md](./static-edge-functions.md) - Add redirects, rewrites, and custom headers via Lambda@Edge
|
|
151
|
+
- [static-full.md](./static-full.md) - Full configuration reference with all options
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# CloudFront Redirects, Rewrites, and Custom Headers with Lambda@Edge
|
|
2
|
+
|
|
3
|
+
Add URL redirects, path rewrites, and custom HTTP response headers to your CloudFront distribution - no origin round-trip required. Thunder deploys [Lambda@Edge](https://aws.amazon.com/lambda/edge/) functions automatically when you configure these options, running your rules at AWS edge locations worldwide.
|
|
4
|
+
|
|
5
|
+
Two functions are created:
|
|
6
|
+
|
|
7
|
+
| Function | CloudFront Event | Purpose |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `RedirectRewriteFunction` | `viewer-request` | Evaluates redirects and rewrites before the cache |
|
|
10
|
+
| `HeadersFunction` | `viewer-response` | Injects custom headers into responses |
|
|
11
|
+
|
|
12
|
+
> Lambda@Edge functions are always deployed to `us-east-1` and replicated globally by CloudFront.
|
|
13
|
+
|
|
14
|
+
## Redirects
|
|
15
|
+
|
|
16
|
+
A redirect returns an HTTP `301 Moved Permanently` response, causing the browser to navigate to a new URL. Use redirects when a URL has permanently moved.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Cdk, Static, type StaticProps } from '@thunder-so/thunder';
|
|
20
|
+
|
|
21
|
+
const config: StaticProps = {
|
|
22
|
+
// ...core config...
|
|
23
|
+
|
|
24
|
+
redirects: [
|
|
25
|
+
// Static redirect
|
|
26
|
+
{ source: '/home', destination: '/' },
|
|
27
|
+
|
|
28
|
+
// Wildcard - captures everything after /guide/
|
|
29
|
+
{ source: '/guide/*', destination: '/docs/*' },
|
|
30
|
+
|
|
31
|
+
// Named placeholders
|
|
32
|
+
{ source: '/blog/:year/:month', destination: '/posts/:year/:month' },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Pattern Syntax
|
|
38
|
+
|
|
39
|
+
| Pattern | Matches |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `/about` | Exact path `/about` |
|
|
42
|
+
| `/blog/*` | Any path starting with `/blog/` |
|
|
43
|
+
| `/user/:id` | `/user/123`, `/user/abc`, etc. |
|
|
44
|
+
| `/a/:x/b/:y` | `/a/foo/b/bar` → placeholders `x=foo`, `y=bar` |
|
|
45
|
+
|
|
46
|
+
Wildcards (`*`) and placeholders (`:name`) can be used in both `source` and `destination`. Placeholders are positional - they map by name between source and destination.
|
|
47
|
+
|
|
48
|
+
## Rewrites
|
|
49
|
+
|
|
50
|
+
A rewrite changes the path the request is forwarded to internally, without changing the URL in the browser. Use rewrites to serve a different file while keeping the original URL visible.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const config: StaticProps = {
|
|
54
|
+
// ...
|
|
55
|
+
|
|
56
|
+
rewrites: [
|
|
57
|
+
// Serve index.html for all app routes (SPA fallback)
|
|
58
|
+
{ source: '/app/*', destination: '/index.html' },
|
|
59
|
+
|
|
60
|
+
// Map a vanity URL to a real path
|
|
61
|
+
{ source: '/profile/:username', destination: '/user/:username' },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### SPA Fallback Pattern
|
|
67
|
+
|
|
68
|
+
For client-side routed SPAs, rewrite all unmatched paths to `index.html`:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
rewrites: [
|
|
72
|
+
{ source: '/*', destination: '/index.html' },
|
|
73
|
+
],
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> Rewrites run after redirects. If a path matches a redirect rule, the rewrite is never evaluated.
|
|
77
|
+
|
|
78
|
+
## Custom HTTP Headers
|
|
79
|
+
|
|
80
|
+
Use `headers` to inject or override HTTP response headers for specific path patterns. This runs at the `viewer-response` stage, so it applies to both cached and uncached responses.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const config: StaticProps = {
|
|
84
|
+
// ...
|
|
85
|
+
|
|
86
|
+
headers: [
|
|
87
|
+
// Cache HTML for 10 minutes
|
|
88
|
+
{ path: '/*', name: 'Cache-Control', value: 'public, max-age=600' },
|
|
89
|
+
|
|
90
|
+
// Long-lived cache for hashed assets
|
|
91
|
+
{ path: '/assets/*', name: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
|
|
92
|
+
|
|
93
|
+
// Restrict embedding to same origin
|
|
94
|
+
{ path: '/**', name: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
95
|
+
|
|
96
|
+
// CORS for a specific API path
|
|
97
|
+
{ path: '/api/*', name: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Path Syntax
|
|
103
|
+
|
|
104
|
+
| Path | Matches |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `/*` | Root-level paths only |
|
|
107
|
+
| `/**` | All paths including nested |
|
|
108
|
+
| `/blog/*` | All paths under `/blog/` |
|
|
109
|
+
| `/assets/*.{js,css}` | JS and CSS files under `/assets/` |
|
|
110
|
+
|
|
111
|
+
### Header Evaluation Order
|
|
112
|
+
|
|
113
|
+
Headers are applied in array order. Later entries for the same header name on the same path will overwrite earlier ones.
|
|
114
|
+
|
|
115
|
+
> Custom headers defined here are applied **after** the built-in security headers policy. To override a default security header (e.g. `X-Frame-Options`), specify it explicitly here.
|
|
116
|
+
|
|
117
|
+
## Default Security Headers
|
|
118
|
+
|
|
119
|
+
These are always applied by the CloudFront Response Headers Policy, regardless of your `headers` config:
|
|
120
|
+
|
|
121
|
+
| Header | Default |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` |
|
|
124
|
+
| `X-Frame-Options` | `DENY` |
|
|
125
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
126
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
127
|
+
| `X-XSS-Protection` | `1; mode=block` |
|
|
128
|
+
| `Content-Security-Policy` | `default-src 'self'; style-src https: 'unsafe-inline'; script-src https: 'unsafe-inline' 'wasm-unsafe-eval'; ...` |
|
|
129
|
+
|
|
130
|
+
Default CORS headers (applied to all origins):
|
|
131
|
+
|
|
132
|
+
| Header | Default |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `Access-Control-Allow-Origin` | `*` |
|
|
135
|
+
| `Access-Control-Allow-Methods` | `GET, HEAD, OPTIONS` |
|
|
136
|
+
| `Access-Control-Allow-Headers` | `*` |
|
|
137
|
+
| `Access-Control-Max-Age` | `600` |
|
|
138
|
+
|
|
139
|
+
## Full Example
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { Cdk, Static, type StaticProps } from '@thunder-so/thunder';
|
|
143
|
+
|
|
144
|
+
const config: StaticProps = {
|
|
145
|
+
env: { account: '123456789012', region: 'us-east-1' },
|
|
146
|
+
application: 'myapp',
|
|
147
|
+
service: 'web',
|
|
148
|
+
environment: 'prod',
|
|
149
|
+
rootDir: '.',
|
|
150
|
+
outputDir: 'dist',
|
|
151
|
+
|
|
152
|
+
domain: 'app.example.com',
|
|
153
|
+
globalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abc-123',
|
|
154
|
+
hostedZoneId: 'Z1234567890ABC',
|
|
155
|
+
|
|
156
|
+
redirects: [
|
|
157
|
+
{ source: '/old-page', destination: '/new-page' },
|
|
158
|
+
],
|
|
159
|
+
|
|
160
|
+
rewrites: [
|
|
161
|
+
{ source: '/app/*', destination: '/index.html' },
|
|
162
|
+
],
|
|
163
|
+
|
|
164
|
+
headers: [
|
|
165
|
+
{ path: '/**', name: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
166
|
+
{ path: '/assets/*', name: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
new Static(new Cdk.App(), 'myapp-web-prod-stack', config);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
**Redirects not firing:** Lambda@Edge logs appear in CloudWatch in the region closest to the viewer, not `us-east-1`. Check the relevant regional log group `/aws/lambda/us-east-1.RedirectRewriteFunction`.
|
|
176
|
+
|
|
177
|
+
**Headers not appearing:** Verify the path pattern matches your URL. Use `/**` to match all paths including nested ones.
|
|
178
|
+
|
|
179
|
+
**CSP blocking resources:** The default CSP is strict. Override it with a `headers` entry:
|
|
180
|
+
```typescript
|
|
181
|
+
{ path: '/**', name: 'Content-Security-Policy', value: 'your-custom-policy' }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Related
|
|
185
|
+
|
|
186
|
+
- [static-basic.md](./static-basic.md) - Basic setup
|
|
187
|
+
- [static-full.md](./static-full.md) - Full configuration reference
|