Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
name: aws-lambda-web-functions
description: >
Creates and deploys Node.js HTTP web applications on AWS Lambda Web Functions. Guides users through building web apps and APIs with Express, Fastify, or Hono, packaging, deploying via the aws lambda-web CLI, and iterating. Triggers when user wants to deploy a website or web app to AWS as serverless, deploy a Node.js HTTP server to AWS, build a serverless website or API with Node.js, deploy Express/Hono/Fastify apps, set up multi-region HTTP endpoints, connect a Node.js web app to private VPC resources, or explicitly mentions Lambda Web Functions. Does NOT trigger for non-Node.js runtimes, event-driven patterns (SQS, Kinesis, S3 events), or container deployments.
version: 1
Creates and deploys Node.js HTTP web applications on AWS Lambda Web Functions. Guides users through building web apps and APIs with Express, Fastify, or Hono, packaging, deploying via the aws lambda-web CLI, and iterating. Triggers when user wants to deploy a website or web app to AWS as serverless, deploy a Node.js HTTP server to AWS, build a serverless website or API with Node.js, deploy Express/Hono/Fastify apps, set up multi-region HTTP endpoints, connect a Node.js web app to private VPC resources, or explicitly mentions Lambda Web Functions. Does NOT trigger for non-Node.js runtimes or frameworks such as Flask or Django, event-driven patterns (SQS, Kinesis, S3 events), or container deployments.
version: 2
---

# AWS Lambda Web Functions
Expand Down Expand Up @@ -234,7 +234,7 @@ Two consequences worth planning around:
- **I/O-bound workloads are the economical case.** A handler that spends most of its life waiting on the network, sharing an environment with other requests, costs less here than the same handler billed per-invoke on wall-clock duration.
- **CPU-bound workloads give that advantage back.** Heavy rendering, image processing or crypto burns billable CPU wherever it runs, and an environment is allocated 2 vCPU — of which a single-threaded Node process uses only one (see Anti-Patterns). For sustained high-volume CPU work, especially where committed-use discounts apply, compare **`aws-lambda-managed-instances`**.

An endpoint's scaling controls `scalingConfig.maxEnvironments` and `throttleConfig.rateLimit` can be used to limit potential cost. See [scaling-and-concurrency.md](references/scaling-and-concurrency.md) for more details.
An endpoint's scaling controls `scalingConfig.maxEnvironments` and `throttleConfig.rateLimit` can be used to limit potential cost. Set them only when the user asks for a cap. See [scaling-and-concurrency.md](references/scaling-and-concurrency.md) for more details.

## IAM Setup

Expand Down Expand Up @@ -267,9 +267,10 @@ What it looks like → why it fails → what to do instead.
| Hardcoding `app.listen(3000)`, or binding `localhost` / `127.0.0.1` | The runtime probes the exact address it advertises. A mismatched port or a loopback-only bind never answers that probe | Parse `AWS_LAMBDA_HTTP_ENDPOINT` and bind that host and port |
| Module-level mutable state holding per-request data — a `currentUser`, a request-scoped cache, a counter | One execution environment serves many concurrent requests in a single process, so requests read each other's values. This is a correctness and data-leak bug, not a performance one | Keep per-request data in the request scope; share only immutable config and SDK clients at module level |
| Writing session or upload state to `/tmp` and expecting it on the next request | `/tmp` is per environment and shared between concurrent requests; the next request may land on a different environment, or a different Region on a multi-region endpoint | Keep state in DynamoDB, S3 or ElastiCache; use unique filenames for genuinely temporary files |
| Treating the endpoint URL as access control — "nobody knows this domain" | An `ApplicationManaged` endpoint is reachable by anyone on the internet with the URL. No AWS-layer auth, no built-in WAF, and the URL is not a secret | Implement auth in the app, or use `IamAuth`; add CloudFront + WAF for L7 protection → [iam-and-security.md](references/iam-and-security.md) |
| Treating the endpoint URL as access control — "nobody knows this domain" | An `ApplicationManaged` endpoint is reachable by anyone on the internet with the URL. No AWS-layer auth, no built-in WAF, and the URL is not a secret | Implement auth in the app, or use `IamAuth`; to let only CloudFront in, use `IamAuth` with CloudFront OAC and a resource-based policy → [domains-and-routing.md](references/domains-and-routing.md); add WAF for L7 protection → [iam-and-security.md](references/iam-and-security.md) |
| Secrets in `serviceConfig.environmentVariables` | `get-web-function-revision` returns environment variables in plaintext to anyone with read access to the revision | Keep the secret in Secrets Manager or SSM, pass only its name or path, fetch at runtime |
| Running `npm ci --omit=dev` before the build step | Pruning devDependencies deletes the bundler, so the build then fails with `esbuild: command not found` | Build first, prune afterwards — or zip the bundled output → [deployment.md](references/deployment.md) |
| Relying on the runtime for `@aws-sdk/*`, or marking it external (`externalModules`, `--external:@aws-sdk/*`), as on Event Functions | The Web Functions runtime does not include the AWS SDK, so the import fails at startup and every request returns 502 | Package the `@aws-sdk/*` clients you import like any other dependency → [deployment.md](references/deployment.md) |
| Expecting an `update-*` call to change code, runtime, timeout, env vars or concurrency in place | All of those live on the immutable revision | Create a new revision. Only endpoint-level settings (auth, weights, scaling, throttle) update in place |
| Re-uploading to the same S3 key and expecting a redeploy | A revision is pinned to an object version; CloudFormation reports "No changes to deploy" | Use a new key, or pass the new `versionId` |
| Concluding the feature does not exist because `aws lambda-web` is missing | Absence means an outdated CLI, not a missing capability | Update the AWS CLI → [deployment.md](references/deployment.md) |
Expand Down Expand Up @@ -308,10 +309,10 @@ Match on meaning, not exact wording — any trigger word in a row should pull th
| [getting-started.md](references/getting-started.md) | first deploy, get started, set up, prerequisites, hello world, `--hello-world`, starter app, new project, from scratch, S3 code bucket, execution role, bootstrap, my first web function |
| [frameworks.md](references/frameworks.md) | Express, Fastify, Hono, TypeScript, `.ts`, type stripping, ESM, `type: module`, scaffold, project structure, entry point, `index.js`, router, middleware |
| [deployment.md](references/deployment.md) | deploy, redeploy, ship, release, CLI, `aws lambda-web`, `aws lambda-web deploy`, one command, revision, endpoint, packaging, ZIP, `npm ci`, timeout, environment variables, env vars, canary, blue/green, traffic weights, rollback, `versionId` |
| [scaling-and-concurrency.md](references/scaling-and-concurrency.md) | scaling controls, concurrency, `maxConcurrencyPerEnvironment`, `scalingConfig`, `maxEnvironments`, `throttleConfig`, `rateLimit`, throttling, RPS limit, vCPU quota |
| [infrastructure-as-code.md](references/infrastructure-as-code.md) | CloudFormation, CFN, CDK, `CfnResource`, escape hatch, SAM, Terraform, HashiCorp, HCL, infrastructure as code, IaC, template, stack, `AWS::Lambda::WebFunction`, `AWS::Lambda::NetworkConnector`, VPC egress in a stack, repeatable deployment, pipeline |
| [scaling-and-concurrency.md](references/scaling-and-concurrency.md) | scaling controls, concurrency, `maxConcurrencyPerEnvironment`, `scalingConfig`, `maxEnvironments`, `throttleConfig`, `rateLimit`, throttling, RPS limit, vCPU quota, load test, `Platform.EndpointMaxEnvThrottle` |
| [infrastructure-as-code.md](references/infrastructure-as-code.md) | CloudFormation, CFN, CDK, `CfnResource`, escape hatch, SAM, Terraform, HashiCorp, HCL, infrastructure as code, IaC, template, stack, `AWS::Lambda::WebFunction`, `AWS::Lambda::NetworkConnector`, VPC egress in a stack, `AWS::Lambda::ResourcePolicy`, repeatable deployment, pipeline |
| [static-assets.md](references/static-assets.md) | static files, static assets, CSS, JS bundle, images, favicon, `public/`, `express.static`, SPA, single-page app, client-side routing, fallback route, bundling, esbuild, Vite, cache headers |
| [domains-and-routing.md](references/domains-and-routing.md) | multi-region, multiregion, global, latency routing, Route53, domain, custom domain, hostname, HTTPS, TLS, endpoint type, `HomeRegion`, `MultiRegion`, `PerRegion`, CORS, auth type, `ApplicationManaged`, `IamAuth` |
| [domains-and-routing.md](references/domains-and-routing.md) | multi-region, multiregion, global, latency routing, Route53, domain, custom domain, hostname, HTTPS, TLS, endpoint type, `HomeRegion`, `MultiRegion`, `PerRegion`, CORS, auth type, `ApplicationManaged`, `IamAuth`, CloudFront, OAC, origin access control, resource-based policy |
| [iam-and-security.md](references/iam-and-security.md) | IAM, policy, permissions, role, trust policy, `PassRole`, least privilege, AccessDenied, security, auth, authentication, authorization, public endpoint, WAF, DDoS, rate limiting, secrets, KMS, encryption, security headers, `helmet` |
| [observability.md](references/observability.md) | logs, logging, log group, log level, CloudWatch, metrics, monitoring, alarms, tracing, CloudTrail, audit, structured logging, health check, which region served, debug in production |
| [architecture-patterns.md](references/architecture-patterns.md) | SSR, server-side rendering, React, JSX, streaming, SSE, server-sent events, LLM, AI gateway, token streaming, long-lived connection, webhook, background task, fire and forget, API backend, DynamoDB, connection pooling, thread safety, concurrency model |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const [HOST, PORT] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || '0.0.0.0:3000').sp
app.listen(PORT, HOST);
```

The runtime does not include the AWS SDK, so install the clients as dependencies
(`npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb`) and package them →
[deployment.md](references/deployment.md).

## SSR (Server-Side Rendering)

Render HTML on the server per request — a plain `index.js` HTTP server, no build step and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ zip -r function.zip index.js package.json node_modules/ public/ \

Include: `index.js`, package.json, node_modules/, public/. See Service Limits in [SKILL.md](SKILL.md) for the package size cap.

**Package the AWS SDK.** The Web Functions runtime does not include the AWS SDK, so package the
`@aws-sdk/*` clients your code imports like any other dependency: list them in `dependencies` (the
`npm ci --omit=dev` step removes `devDependencies`), or bundle them. Do not mark them external
(`externalModules: ['@aws-sdk/*']`, `--external:@aws-sdk/*`), which is common for Event Functions.
A missing client fails the import at startup and every request returns 502.

**If you have a build step** (esbuild for JSX or TypeScript), build *before* pruning. `npm ci --omit=dev` deletes the bundler along with the other devDependencies, so a later `npm run build` fails with `esbuild: command not found`:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ CloudFront in front.
One distribution solves four separate problems: a custom domain, HTTP/2 at the edge, compression,
and L7 protection. If you need any one of them, this is the shape.

1. **Origin.** Use the endpoint's `domainName` as a custom origin, HTTPS only. It is a public
hostname, so there is no origin access identity to configure.
1. **Origin.** Use the endpoint's `domainName` as a custom origin, HTTPS only. To make CloudFront
the only caller, use origin access control (OAC) as below.
2. **Certificate.** Request or import an ACM certificate **in `us-east-1`**, which is where
CloudFront reads certificates from regardless of where the function runs, and attach it to the
distribution as an alternate domain name.
Expand All @@ -149,8 +149,53 @@ and L7 protection. If you need any one of them, this is the shape.
6. **L7 protection.** Attach AWS WAF for rate limiting, bot control and IP reputation. The managed
domain has none of its own, and `throttleConfig.rateLimit` is endpoint-wide rather than
per-client.
7. **Logging.** Enable CloudFront standard logging and AWS WAF logging to an encrypted
destination, so edge traffic is available for audit and incident response. The function's own
logs record only requests that reach it.

The endpoint stays publicly reachable on its generated name after you put CloudFront in front, so
the distribution is not by itself an access control.
If only CloudFront should reach the app, enforce that in the app, for example by requiring a shared
secret header that the distribution adds and the app verifies.
### Restricting the endpoint to CloudFront with OAC

The endpoint stays reachable on its generated name after you put CloudFront in front, so the
distribution is not by itself an access control. To let only your distribution invoke it:

1. **Set the endpoint to `IamAuth`.** Lambda does not authenticate `ApplicationManaged` requests
or evaluate a resource policy for them, so an `ApplicationManaged` endpoint stays public even
behind CloudFront with OAC.
2. **Create an origin access control** with origin type `lambda` and signing behavior
`always-amz-auth` (CloudFront rejects other signing behaviors for Web Function origins), and
attach it to the origin. CloudFront then signs every origin request, so viewers never sign.
3. **Grant CloudFront invoke in the function's resource-based policy**, scoped to the endpoint and
to your distribution with `aws:SourceArn` and `aws:SourceAccount`:

```json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": "lambda:InvokeWebFunctionEndpoint",
"Resource": "arn:aws:lambda:{region}:{account}:web-function/{name}/endpoint/{endpoint}",
"Condition": {"StringEquals": {
"aws:SourceAccount": "{account}",
"aws:SourceArn": "arn:aws:cloudfront::{account}:distribution/{distribution-id}"
}}
}]
}
```

```bash
aws lambda-web put-resource-policy \
--resource-arn arn:aws:lambda:{region}:{account}:web-function/{name} \
--policy file://policy.json --region {home-region}
```

The policy attaches to the **function** ARN; each statement scopes `Resource` to an endpoint ARN.
A function has one policy and `put-resource-policy` replaces it, so to add a statement read the
current one with `get-resource-policy`, append to `Statement`, and pass its `revisionId` with
`--revision-id`.

App-level auth still works behind OAC. Lambda strips only an `Authorization` value that is itself an
AWS signature (`AWS4-...`), so forward the viewer's `Authorization` header in the origin request
policy and a JWT or bearer token reaches the app. For the stack form, see
[infrastructure-as-code.md](references/infrastructure-as-code.md).
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Web Functions endpoints are **public by default** (`ApplicationManaged`). Treat
- **App-owned auth.** For `ApplicationManaged`, implement authentication and authorization in the app (validate a JWT / session / API key on every protected route).
- **Input validation.** Validate and sanitize all request input (path, query, headers, body) — you're taking raw internet traffic. Guard against injection, oversized payloads, and malformed input.
- **Rate limiting / abuse protection.** Set `throttleConfig.rateLimit` on the endpoint to cap requests per second, and cap `scalingConfig.maxEnvironments` to bound blast radius. That throttle is endpoint-wide, not per-client, so per-client quotas still belong in the app.
- **L7 protection (DDoS / bots / WAF).** No built-in WAF. For public-facing apps that need L7 DDoS/bot protection, place your own CloudFront distribution (with AWS WAF) in front of the endpoint.
- **L7 protection (DDoS / bots / WAF).** No built-in WAF. For public-facing apps that need L7 DDoS/bot protection, place your own CloudFront distribution (with AWS WAF) in front of the endpoint. To make the distribution the only caller, use an `IamAuth` endpoint with CloudFront OAC and a resource-based policy → [domains-and-routing.md](references/domains-and-routing.md).
- **Security headers.** For browser-facing responses, set standard headers — `Content-Security-Policy`, `Strict-Transport-Security` (HSTS), `X-Frame-Options`, `X-Content-Type-Options`. Use a middleware like `helmet` for Express/Fastify rather than setting them by hand.
- **Enforce TLS on the code bucket.** For production, add a `Deny` statement on `s3:*` for both the bucket and its objects conditioned on `aws:SecureTransport` being `false`, so artifacts cannot be fetched over plain HTTP.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,54 @@ to it. The CLI path does not do this: `aws lambda-web deploy` carries the previo
connector ARN forward verbatim, and that ARN is version-qualified, so a redeploy keeps the old
version.

## CloudFront-Only Access in a Stack

To let only a CloudFront distribution invoke an `IamAuth` endpoint, attach the function's
resource-based policy with `AWS::Lambda::ResourcePolicy`. The property is **`ResourceArn`**, set to
the web-function ARN; `FunctionResourceArn` is not a property and CloudFormation rejects it. The
distribution's origin uses an `AWS::CloudFront::OriginAccessControl` with
`OriginAccessControlOriginType: lambda` and `SigningBehavior: always-amz-auth`. Why each piece is
needed: [domains-and-routing.md](references/domains-and-routing.md).

```yaml
OriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: {name}-oac
OriginAccessControlOriginType: lambda
SigningBehavior: always-amz-auth
SigningProtocol: sigv4
# On the distribution's origin: OriginAccessControlId: !GetAtt OriginAccessControl.Id

CloudFrontInvokePolicy:
Type: AWS::Lambda::ResourcePolicy
Properties:
ResourceArn: !GetAtt WebFunction.FunctionArn # the web-function/... ARN
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: cloudfront.amazonaws.com }
Action: lambda:InvokeWebFunctionEndpoint
Resource: !GetAtt WebFunctionEndpoint.EndpointArn
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
aws:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}"
```

`aws:SourceArn` must be the distribution's full ARN, so build it with `!Sub` as above.
`!Ref Distribution` returns only the distribution ID and `AWS::CloudFront::Distribution` has no
`Arn` attribute, so either form gives a condition that never matches and CloudFront gets 403.

In CDK the generated L1 is `CfnResourcePolicy` (`aws-cdk-lib/aws-lambda`), whose property is
`resourceArn`; pass `fn.attrFunctionArn` from `CfnWebFunction`. A literal web-function ARN string
makes `cdk synth` warn that `ResourceArn` does not match the pattern
(`CloudFormation-Validate::F3017` and `F3031`), because CDK's built-in validation carries an older
schema; cfn-lint can report the same. The template is valid and the stack deploys; do not switch
to a custom resource to set the policy.

## CDK

There is no L2 construct for Web Functions. If your `aws-cdk-lib` has no generated L1
Expand Down
Loading
Loading