Web Function Construct¶
The WebFunctionConstruct deploys an AWS Lambda Web Function: an ordinary HTTP server packaged as a zip, uploaded to S3, and run behind a managed HTTPS endpoint. It owns one Web Function's lifecycle β the function, its current immutable revision, its execution role and log groups (each optional), one default endpoint plus any added with AddEndpoint, and the function's single resource-based policy.
It is intentionally workload-neutral: Next.js, ASP.NET Core, and Blazor Static SSR deploy through the same public API, and the differences live in the artifacts and configuration only. See the workload recipes.
Lambda Web Functions are a new AWS feature
CloudFormation exposes them as AWS::Lambda::WebFunction, WebFunctionRevision, WebFunctionEndpoint, and AWS::Lambda::ResourcePolicy. AWS CDK 2.274.0 has only L1 support; this construct wraps the L1s with secure wiring, synthesis-time validation of invalid combinations, and CDK-idiomatic grants.
Features¶
S3-backed artifacts: Deploy from an existing S3 object with
WebFunctionCode.FromBucket, with optional object-version pinning.Explicit endpoint authentication: Every endpoint chooses
IamAuthorApplicationManaged; there is no default auth type.Endpoint-scoped grants:
GrantInvokegrantslambda:InvokeWebFunctionEndpointon a specific endpoint ARN.CloudFront OAC integration:
WebFunctionOriginreturns a CloudFrontIOriginthat configures origin access control and the required resource-policy statement.One resource policy per function: Every resource-policy contribution flows through a single policy owned by the construct.
Immutable revisions: Each deployment creates a new revision; endpoints move to it without interruption.
Configurable role and logs: Bring your own execution role and log groups, or let the construct create the centralized group and its home-Region
.localcompanion.Consumer test helpers: Fluent
Templateassertions and a props builder in theTestingnamespace.
Intentional Non-Goals (v1)¶
- Building, publishing, zipping, or uploading artifacts. The S3 object must exist before deployment.
WebFunctionCode.FromAsset(designed for later; not built).- Weighted revisions, canary or blue-green routing, traffic shifting, or retaining previous revisions.
- Network connectors (VPC egress).
- Whole-function imports (
FromWebFunctionAttributes); only endpoints can be imported. - Owning or creating CloudFront distributions, ACM certificates, Route 53 records, or WAF. Composition with those is plain CDK.
- Workload-specific constructs or CloudFront behavior helpers.
- Caller-side request signing (SigV4 in your application) or injecting caller configuration.
- Alarms and dashboards.
- Revision encryption with a customer-managed key (
KmsKeyArn). Deferred; see Known Limitations. S3 artifact encryption (SSE-S3/SSE-KMS on the code bucket) is supported.
Prerequisites¶
- AWS CDK for .NET 2.274.0 or later (
Amazon.CDK.Lib). This release adds the enum values the construct needs for OAC signing. - A region where Lambda Web Functions are available.
- A versioned S3 bucket that holds the application zip, and an artifact uploaded before you deploy.
aws-cdkbootstrapped in the account/region if you use CDK assets elsewhere in the same app.
Basic Usage¶
using Amazon.CDK;
using Amazon.CDK.AWS.S3;
using LayeredCraft.Cdk.Constructs;
using LayeredCraft.Cdk.Constructs.Models;
public class MyStack : Stack
{
public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
{
// Artifacts live in a versioned bucket (owned elsewhere or imported by name).
var artifacts = Bucket.FromBucketName(this, "artifacts", "acme-artifacts");
var web = new WebFunctionConstruct(this, "web", new WebFunctionConstructProps
{
FunctionName = "acme-web",
Code = WebFunctionCode.FromBucket(artifacts, "1.2.3/web.zip", new WebFunctionCodeFromBucketOptions
{
BucketPolicy = WebFunctionCodeBucketPolicy.ExternallyManaged,
}),
Endpoint = new WebFunctionEndpointOptions
{
AuthType = WebFunctionAuthType.IamAuth,
},
});
}
}
Configuration Properties¶
Required Properties¶
| Property | Type | Description |
|---|---|---|
FunctionName | string | Web Function name, 1β64 chars matching ^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?$ |
Code | WebFunctionCode | The artifact source, created with WebFunctionCode.FromBucket |
Endpoint | IWebFunctionEndpointOptions | The default endpoint; AuthType is required |
Optional Properties¶
| Property | Type | Default | Description |
|---|---|---|---|
Runtime | WebFunctionRuntime | NodeJs24X | The only supported runtime is nodejs24.x |
EntryPoint | string? | null | Exported as AWS_LAMBDA_NODEJS_ENTRYPOINT; defaults to index.js in the zip root |
EnvironmentVariables | IReadOnlyDictionary<string,string> | {} | Environment variables for the revision |
Timeout | Duration | 30s | Revision timeout, 3β900 seconds |
MaxConcurrencyPerEnvironment | int? | null | Max concurrent requests per environment, 1β128 (AWS default 64) |
Description | string? | null | Revision description |
Role | IRole? | null | Execution role; when null a role with AWSLambdaBasicExecutionRole is created |
Logs | WebFunctionLogs | WebFunctionLogs.Create() | Where revision logs go. Create() manages the centralized group and its home-Region .local companion |
ApplicationLogLevel | ApplicationLogLevel? | null | Application log level for the revision |
SystemLogLevel | SystemLogLevel? | null | System log level for the revision |
Endpoint options (WebFunctionEndpointOptions)¶
| Property | Type | Default | Description |
|---|---|---|---|
EndpointName | string | "default" | Unique per function, 1β64 chars |
AuthType | WebFunctionAuthType | (required) | IamAuth or ApplicationManaged |
EndpointType | WebFunctionEndpointType | HomeRegion | HomeRegion, MultiRegion, or PerRegion |
Regions | IReadOnlyList<string> | [] | Required for MultiRegion/PerRegion; at least one non-home region |
MaxEnvironments | int? | null | 2β10000; null means the account vCPU quota only |
RateLimit | int? | null | 0, 100..1000 step 100, or 2000..10000 step 1000 |
Description | string? | null | Endpoint description |
S3 Artifact Requirements¶
WebFunctionCode.FromBucket(bucket, key, options) is the only supported source. The bucket must have versioning enabled, and the object must exist before CloudFormation creates the revision β the construct cannot verify that at synthesis time.
- Version pinning: set
WebFunctionCodeFromBucketOptions.ObjectVersionto pin an exact object version. If you omit it, Lambda pins the latest version at revision-creation time. - Read access: Lambda reads the artifact with the Lambda service principal (
lambda.amazonaws.com), not with the caller. When the construct manages the policy (Automatic) the statement grantss3:GetObjectands3:GetObjectVersionand is scoped with bothStringEquals aws:SourceAccount(the function's account) andArnLike aws:SourceArnof the formarn:aws:lambda:{region}:{account}:web-function/{FunctionName}/revision/*. An externally managed policy must grant at least those actions with equivalent scoping. AutomaticvsExternallyManaged:Automatic(default) adds that statement and makes the revision depend on the resulting bucket policy. It manages a bucket owned in this app, including a bucket owned by another stack when that stack's account and region are explicitly resolvable (for example a stack created with an explicitEnv). If the bucket is imported, or is owned by another stack whose required environment information cannot be resolved,AddToResourcePolicyreports that nothing was added and synthesis fails asking you to switch toExternallyManaged.ExternallyManagedadds nothing; the bucket's owner must already grant the service-principal read (for example a long-lived artifact bucket deployed and managed in its own stack).
- SSE-KMS: if the code bucket uses SSE-KMS, its key policy must allow
lambda.amazonaws.comkms:Decrypt, because Lambda reads with its service principal. CDK-managedaws/s3and DSSE-managed keys cannot grant that, so an owned bucket using them is rejected. This is separate from the revision's ownKmsKeyArn, which is not supported in v1. - Cross-account / cross-region buckets: allowed within the same partition (not an opt-in region). Cross-account access needs a bucket-policy grant.
- Object lifecycle: deleting the pinned version (or removing read access) moves revisions to permanent
Failed; requests then receive409 Platform.RevisionNotActive. - CDK bootstrap bucket is not usable: the bootstrap asset bucket does not grant the service-principal read, which is why v1 is
FromBucketonly.
The artifact must exist before you deploy
The object must exist before CloudFormation creates the revision, and a bucket created in the same stack as the function does not contain it at first deployment. In practice the artifact is uploaded before you deploy the function stack. A separately deployed artifact bucket is a good fit (deploy the bucket stack, upload the artifact, then deploy the function stack), but any bucket you populate first β including a runtime upload from a pipeline or a bucket shared across applications β works equally well.
Execution Role and Logging¶
Execution role¶
- Construct-created role (
Roleisnull): the construct creates anExecutionRolewith the AWS managedAWSLambdaBasicExecutionRolepolicy. That policy includeslogs:CreateLogGroup,logs:CreateLogStream, andlogs:PutLogEvents, so a created role can write to the default log groups and to a managed.localcompanion. - Supplied role (
Roleis notnull): the construct uses it unchanged and never attaches the managed policy. Whether it adds CloudWatch Logs statements depends on the logging mode and the log-group name; see Execution-role permissions. When it adds nothing, you must configure the role yourself. - Imported immutable role (
Role.FromRoleArn(..., new FromRoleArnOptions { Mutable = false })): the construct never mutates it. You manage every log permission.
ApplicationLogLevel and SystemLogLevel reuse Amazon.CDK.AWS.Lambda.ApplicationLogLevel and SystemLogLevel.
Two CloudWatch Logs groups per Web Function¶
Every Lambda Web Function writes to two CloudWatch Logs groups:
| Group | Default name | Region | Purpose |
|---|---|---|---|
| Centralized | /aws/lambda/web/{FunctionName}, or the group supplied to WebFunctionLogs.Use | Stack (home) Region | Collects events from every Region that serves requests. Use it for normal queries, metric filters, and alarms. |
| Companion | {centralized-name}.local | Every Region that serves a request; the construct manages only the home-Region one | Receives events in the serving Region first. Lambda then centralizes them into the home-Region group. Use the serving Region's .local group for the lowest-latency Live Tail. |
Lambda writes to the companion in the serving Region and centralizes those events into the home-Region group. Events are not removed from the companion after centralization, so the same events appear in both groups with the same timestamps and messages. The centralized stream name adds a -{account}-{region} suffix, which is one way to tell the copies apart. CloudWatch Logs bills storage for both groups, and each group's own retention decides how long its copy remains.
No retention means no expiry
AWS does not automatically expire log events in a group that has no retention configured. Lambda-created groups (a Use companion created on first request, and both groups with ServiceDefault()) have no expiry. Configure retention explicitly if you need finite storage.
See the AWS documentation for Lambda Web Function logging and Web Function permissions.
Logging modes¶
WebFunctionConstructProps.Logs is a closed choice of three modes:
| API | Behavior |
|---|---|
WebFunctionLogs.Create(RetentionDays? retention = null, RemovalPolicy? removalPolicy = null, RetentionDays? companionRetention = null) | Creates the centralized group and its home-Region companion |
WebFunctionLogs.Use(ILogGroup logGroup, ILogGroup? companion = null) | Uses caller-supplied groups and creates neither |
WebFunctionLogs.ServiceDefault() | Creates neither group; the service manages both |
After construction, LogGroup is the centralized group (or null for ServiceDefault), and LocalLogGroup is the managed or supplied home-Region companion (or null when it is not managed).
WebFunctionLogs.Create (default)¶
Create creates both the centralized group and its home-Region companion:
using Amazon.CDK;
using Amazon.CDK.AWS.Logs;
using LayeredCraft.Cdk.Constructs;
using LayeredCraft.Cdk.Constructs.Models;
var web = new WebFunctionConstruct(this, "web", new WebFunctionConstructProps
{
FunctionName = "acme-web",
Code = WebFunctionCode.FromBucket(artifacts, "1.2.3/web.zip", new WebFunctionCodeFromBucketOptions
{
BucketPolicy = WebFunctionCodeBucketPolicy.ExternallyManaged,
}),
Endpoint = new WebFunctionEndpointOptions { AuthType = WebFunctionAuthType.IamAuth },
// Retention is independent for the two groups.
Logs = WebFunctionLogs.Create(
retention: RetentionDays.ONE_MONTH, // centralized; default TWO_WEEKS
removalPolicy: RemovalPolicy.RETAIN, // both groups; default DESTROY
companionRetention: RetentionDays.ONE_WEEK), // companion; default THREE_DAYS
});
// Both are construct-created groups in the stack Region.
ILogGroup centralized = web.LogGroup!;
ILogGroup companion = web.LocalLogGroup!;
- The centralized group defaults to
RetentionDays.TWO_WEEKS. - The companion defaults to
RetentionDays.THREE_DAYS. It is a short-lived staging copy whose main use is low-latency Live Tail; three days covers a centralization delay or weekend debugging at modest storage cost. companionRetentionis independent ofretention. Both created groups use the sameRemovalPolicy; there is no separate companion removal policy. The default isRemovalPolicy.DESTROY.- The construct adds explicit dependencies so both groups exist before the revision is created. The revision is also deleted before the groups on teardown.
LocalLogGroupexposes the created companion.
WebFunctionLogs.Use¶
Use(logGroup, companion = null) routes logs to groups you provide. The construct never creates a group in this mode, including a companion.
using Amazon.CDK;
using Amazon.CDK.AWS.Logs;
using LayeredCraft.Cdk.Constructs;
using LayeredCraft.Cdk.Constructs.Models;
// Both groups are caller-owned and can have different retention.
var centralized = new LogGroup(this, "AppLogs", new LogGroupProps
{
LogGroupName = "/acme/web",
Retention = RetentionDays.ONE_MONTH,
});
var companion = new LogGroup(this, "AppLogsLocal", new LogGroupProps
{
LogGroupName = "/acme/web.local",
Retention = RetentionDays.THREE_DAYS,
});
var web = new WebFunctionConstruct(this, "web", new WebFunctionConstructProps
{
FunctionName = "acme-web",
Code = WebFunctionCode.FromBucket(artifacts, "1.2.3/web.zip", new WebFunctionCodeFromBucketOptions
{
BucketPolicy = WebFunctionCodeBucketPolicy.ExternallyManaged,
}),
Endpoint = new WebFunctionEndpointOptions { AuthType = WebFunctionAuthType.IamAuth },
Logs = WebFunctionLogs.Use(centralized, companion),
});
- When the optional companion is supplied, it must have the name
{centralizedName}.local. The construct validates names that are provably literal and throwsArgumentException(parametercompanion) on a mismatch. Tokenized names are accepted rather than rejected speculatively. - When the companion is omitted, the construct does not manage it. Lambda creates
{centralizedName}.localon first request with no expiry unless it already exists, and warning W1 explains that retention remains externally managed.LocalLogGroupisnullin this case. - When the companion is supplied,
LocalLogGroupreferences that group. - Same-stack owned groups receive a revision dependency, so they exist before the revision. Imported groups (
LogGroup.FromLogGroupName/LogGroup.FromLogGroupArn) and groups in another stack get no dependency: they must already exist before the deployment. The construct never invents a cross-stack dependency. - The construct does not stop Lambda from creating a companion in other serving Regions; see Multi-Region endpoints.
WebFunctionLogs.ServiceDefault¶
ServiceDefault() creates neither group and leaves both to the Lambda service:
- Lambda creates
/aws/lambda/web/{FunctionName}and its.localcompanion on first use with default settings. - Neither group has a finite retention: they never expire. The construct does not enforce retention in this mode.
LogGroupandLocalLogGroupare bothnull.- No W1 warning is emitted, because
Useis not involved. - The service-created groups are not stack-owned and outlive the stack.
ServiceDefaultis not suitable when you expect automatically managed retention or CloudFormation-managed deletion.
Ownership and retention¶
| Mode | Centralized group | Home-Region .local companion | Who manages retention and deletion |
|---|---|---|---|
Create(...) | Construct creates | Construct creates | Construct/CloudFormation. Defaults are 14 days and 3 days; both follow the same RemovalPolicy (DESTROY by default, RETAIN keeps both). |
Use(g, c) | Caller supplies | Caller supplies | Caller. The companion's retention is whatever the supplied group declares. |
Use(g) | Caller supplies | Not managed. Lambda creates it on first request unless it already exists. | Centralized: caller. Companion: Lambda default (never expires) until the caller configures it separately. |
ServiceDefault() | Lambda creates on first request | Lambda creates on first request | Lambda defaults (never expires) for both. Neither group is stack-owned or deleted with the stack. |
Do not switch ownership of an existing companion casually: see Upgrading an existing deployment.
Ordering and teardown¶
Create: the revision depends explicitly on both created groups, not on synthesis order. Teardown deletes the revision first, then the groups (subject toRemovalPolicy).Usewith same-stack owned groups: the revision depends on each supplied group that is a resource in the same stack. Imported and cross-stack groups get no dependency and must exist before deployment.- Teardown:
DESTROYremoves both managed groups after the revision;RETAINkeeps both. Service-created and other-Region companions are never removed by this stack.
Execution-role permissions¶
CloudWatch Logs authorizes different actions against different resource types. Keep this distinction when writing your own policies:
| Action | Authorized resource | ARN form |
|---|---|---|
logs:CreateLogGroup | log group | arn:{partition}:logs:{region}:{account}:log-group:{name} |
logs:CreateLogStream | log stream | arn:{partition}:logs:{region}:{account}:log-group:{name}:log-stream:{stream} |
logs:PutLogEvents | log stream | arn:{partition}:logs:{region}:{account}:log-group:{name}:log-stream:{stream} |
ILogGroup.GrantWrite uses β¦:log-group:{name}:*, whose :* covers that group's log streams. The companion grant below uses the documented β¦:log-group:{name}.local:log-stream:* form.
| Execution role | Create | Use, custom or tokenized name | Use, /aws/lambda/* name | ServiceDefault |
|---|---|---|---|---|
Construct-created (ExecutionRole) | AWSLambdaBasicExecutionRole only (no extra statements) | Managed policy + GrantWrite + companion write (+ companion create, when needed) | Managed policy only | Managed policy only |
| Supplied, owned mutable role | Caller-managed | GrantWrite + companion write (+ companion create) written to the role's default policy | Caller-managed | Caller-managed |
Imported, mutable role (FromRoleArn default) | Caller-managed | Same statements in an AWS::IAM::Policy in this stack | Caller-managed | Caller-managed |
Imported, immutable role (Mutable = false) | Caller-managed | Not mutated; caller-managed | Caller-managed | Caller-managed |
The construct adds companion statements only when its existing write trigger applies: Use mode with a log-group name that is a token or is outside /aws/lambda/*. When the statements apply, they are:
- Companion write (G1):
logs:CreateLogStreamandlogs:PutLogEventsonarn:{partition}:logs:{R}:{account}:log-group:{c}.local:log-stream:*. - Companion create (G2, only when Lambda may create the companion):
logs:CreateLogGrouponarn:{partition}:logs:{R}:{account}:log-group:{c}.local. It is added when no companion is supplied, or as soon as any endpoint isPerRegionorMultiRegion. {R}is the stack Region while every endpoint isHomeRegion, and*once any endpoint isPerRegion/MultiRegion.- The construct never grants
logs:CreateLogGroupon the centralized group, and never useslogs:*or alog-group:*resource.CreateLogGroupis not always granted.
Where the construct adds no statements (every Create and ServiceDefault configuration, immutable imported roles, and Use with an /aws/lambda/* name), the role must allow at least the following. A construct-created role already does so through AWSLambdaBasicExecutionRole; a supplied role is caller-managed:
logs:CreateLogStreamandlogs:PutLogEventson the centralized group's log streams,arn:{partition}:logs:{home}:{account}:log-group:{c}:log-stream:*, and on the companion's log streams,arn:{partition}:logs:{R}:{account}:log-group:{c}.local:log-stream:*.logs:CreateLogGrouponarn:{partition}:logs:{R}:{account}:log-group:{c}.localwhen a companion may be created (Use(g),ServiceDefault, or non-home-Region endpoints).- For
ServiceDefault, alsologs:CreateLogGrouponarn:{partition}:logs:{home}:{account}:log-group:{c}.
If the CDK @aws-cdk/aws-iam:minimizePolicies setting combines statements during synthesis, the resulting effective permissions are the same; only the statement layout changes.
Multi-Region endpoints¶
The construct provisions a companion only in the stack (home) Region. When an endpoint is PerRegion or MultiRegion, Lambda may run requests in other Regions and creates a companion group there on first request. Those remote groups:
- Are service-created with no expiry and are not managed by this construct.
- Are not removed by stack teardown.
- Require
logs:CreateLogGroupin those Regions (and write access to their log streams) when the execution role is caller-managed.
Each qualifying endpoint emits warning W2. The construct does not provision cross-Region log groups and does not add cross-stack resources. If you require finite retention outside the home Region, create those regional groups separately (for example in a stack deployed to each Region) and configure their retention there.
Warnings¶
The construct emits two acknowledgeable warnings with stable IDs. Unacknowledged warnings can fail strict synthesis (cdk synth --strict).
| Warning | ID | Emitted when | Scope |
|---|---|---|---|
| W1 | @layeredcraft/cdk-constructs:webFunctionUnmanagedCompanionLogGroup | Logs = WebFunctionLogs.Use(g) without a companion | The WebFunctionConstruct |
| W2 | @layeredcraft/cdk-constructs:webFunctionRemoteCompanionLogGroups | Any endpoint is PerRegion or MultiRegion, in every logging mode | Each qualifying endpoint (default or added) |
W1 is not emitted for Create, Use(g, c), or ServiceDefault. For literal names it reads:
Web Function '{fn}' writes to '{c}', but its companion log group '{c}.local' is not managed by this construct. Lambda creates it on first request with no expiry unless it already exists. Pass the companion to WebFunctionLogs.Use(logGroup, companion) or set its retention separately.
When the function or group name is a token, the message uses descriptive wording instead ("This Web Function writes to a log group whose companion log group is not managed...") and never contains raw token text.
W2 for literal names reads:
Endpoint '{endpoint}' ({type}) runs requests in Regions other than the stack Region. Lambda creates the companion log group '{c}.local' in each of those Regions on first request with no expiry; this construct does not manage log groups outside the stack Region. Set their retention separately. {mode sentence}
The trailing mode sentence is:
| Logging mode | Sentence |
|---|---|
Create | "In the stack Region, the centralized group and its companion are managed by this stack." |
Use(g, c) | "In the stack Region, the caller-supplied companion is used." |
Use(g) | "In the stack Region, the companion is not managed either." |
ServiceDefault | "All of this function's log groups, including the centralized group, are service-managed and never expire." |
Tokenized names use descriptive wording ("This endpointβ¦" and "a per-Region companion log group") and never contain raw token text.
Acknowledge a warning on the construct, the endpoint, or an ancestor such as the stack. CDK applies an acknowledgement to child scopes, so acknowledging on the stack suppresses it for the whole stack:
using Amazon.CDK;
var web = new WebFunctionConstruct(this, "web", props);
// W1: the caller owns the companion and its retention.
Amazon.CDK.Annotations.Of(this).AcknowledgeWarning(
"@layeredcraft/cdk-constructs:webFunctionUnmanagedCompanionLogGroup",
"The companion log group and its retention are managed outside this stack.");
// W2: regional companions are managed out of band.
Amazon.CDK.Annotations.Of(this).AcknowledgeWarning(
"@layeredcraft/cdk-constructs:webFunctionRemoteCompanionLogGroups",
"Regional companion groups are created with retention by our regional logging stack.");
The second argument is the reason recorded with the acknowledgement. Use Amazon.CDK.Annotations (not Amazon.CDK.Assertions.Annotations) to avoid the name clash in a test project.
Upgrading an existing deployment (AlreadyExists)¶
If a Web Function has already served traffic without a managed companion, Lambda may have created {centralizedName}.local already. That includes stacks deployed with earlier 2.6.0-preview versions, a switch from ServiceDefault() to Create(), a switch from Use(g) to Use(g, companion), or a redeploy after the stack was deleted while its service-created groups remained.
A subsequent CloudFormation deployment that tries to create a group with the same physical name fails with AlreadyExists. The construct has no adoption, import, or migration mechanism for existing service-created groups.
Supported remedies:
- Delete the existing service-created group immediately before deploying, after coordinating the deployment window and accepting the loss of its events. This applies to the
.localcompanion, and to the centralized group when moving fromServiceDefault()toCreate(). - Move to a new centralized group name, for example with
WebFunctionLogs.Useand a caller-owned pair, so the managed group does not collide with the service-created name.
Assess existing log retention and operational requirements before changing logging ownership. Do not routinely delete production log groups; deleting a group permanently removes its events.
Function Revisions and Endpoints¶
Revisions¶
Every deploy creates a new immutable revision. Changing the code key/version, environment variables, timeout, role, logging, or description produces a new revision and moves every endpoint to it without interruption. The previous revision is deleted during cleanup. Rollback is a redeploy of an earlier artifact (which creates another new revision); there is no re-selection of an old revision.
Endpoints¶
The construct always creates a DefaultEndpoint (endpoint name default) and exposes it as the DefaultEndpoint property. Add more with AddEndpoint(id, options); each added endpoint has identical semantics to the default one. Endpoint names must be unique per function, so passing EndpointName = "default" to AddEndpoint fails as a duplicate.
AddEndpoint rejects the reserved construct IDs Function, Revision, DefaultEndpoint, ExecutionRole, LogGroup, LocalLogGroup, and ResourcePolicy with an ArgumentException.
var webhooks = web.AddEndpoint("webhooks", new WebFunctionEndpointOptions
{
EndpointName = "webhooks",
AuthType = WebFunctionAuthType.ApplicationManaged,
RateLimit = 100,
});
To change an endpoint's EndpointName, EndpointType, or Regions you cannot update in place β CloudFormation would try to replace a fixed name and fail with "already exists", and the domain changes. Add a new endpoint, move consumers, then remove the old one.
Authentication Modes¶
AuthType is a decision you make on every endpoint:
| Mode | Who can reach the endpoint |
|---|---|
IamAuth | Only callers that SigV4/SigV4A-sign with lambda:InvokeWebFunctionEndpoint and are authorized by an identity policy and/or the function's resource policy. |
ApplicationManaged | Anyone who knows the URL. Lambda does not authenticate the request or evaluate the resource policy. The endpoint stays publicly reachable at its own domain, even behind CloudFront. |
IamAuth is not user authentication
IamAuth is a transport boundary between AWS principals. It never identifies an end user. Lambda strips the SigV4 Authorization header before your server sees it, so application authentication (cookies, JWTs, session tokens) is entirely your server's responsibility. With a CloudFront OAC origin, the viewer's Authorization header passes through unchanged.
ApplicationManaged is appropriate when your application authenticates every request itself (for a genuinely public API). IamAuth is appropriate for service-to-service calls and for endpoints fronted by CloudFront with OAC.
Invocation Grants¶
Use GrantInvoke on an endpoint to authorize callers. An endpoint is an IResource and exposes:
endpoint.GrantInvoke(grantee)β the defaultAutomaticcaller policy.endpoint.GrantInvoke(grantee, new WebFunctionInvokeGrantOptions { CallerPolicy = WebFunctionCallerPolicy.ExternallyManaged }).endpoint.GrantCloudFrontInvoke(distributionArn)β CloudFront-specific (see CloudFront OAC).
GrantInvoke throws for an ApplicationManaged endpoint: there is no IAM boundary to grant against.
Identity policy vs resource policy¶
- A same-account caller needs only an identity policy (an
AWS::IAM::Policyattached to the caller) allowinglambda:InvokeWebFunctionEndpointon the endpoint ARN. - A cross-account caller needs the identity policy and a statement in the function's resource policy.
GrantInvoke writes the correct side for you whenever it can establish ownership, and fails synthesis when it cannot, rather than silently producing a nonfunctional grant.
Owned versus imported principals¶
The construct distinguishes two kinds of grantee:
- Owned mutable identities β
Role,User,Group, orLazyRolecreated in this app (for example, aRoleL2 or aWebFunctionConstructitself). These can receive an identity policy. - Everything else β imported roles/users (
Role.FromRoleArn), principals without a concrete owned identity, and any principal whose account cannot be established. Whether an identity policy may be written depends on the rules below.
Same-stack, environment-agnostic grants¶
Automatic grants are valid when the owned caller and the endpoint belong to the same Stack instance, even when that stack's environment is unresolved. This is specifically about a single stack (or one stack reference) β two separate env-agnostic stacks are not the same stack. Their accounts are unresolved, so an Automatic grant between them is rejected; use ExternallyManaged across such stacks.
Cross-stack and cross-account restrictions¶
Automatic (the default) requires the library to fully configure invocation within this deployment. It rejects, at synthesis:
- Two different stacks where either stack's account is unresolved.
- Two different stacks with different literal accounts.
- Cross-account imported roles.
- Any grantee whose account cannot be established.
For these cases use ExternallyManaged (below) and configure the caller's identity policy in the caller's own account/stack.
Automatic versus ExternallyManaged caller policies¶
CallerPolicy | Behavior |
|---|---|
Automatic | The library must write a working grant. If it cannot fully configure invocation, synthesis fails with a message naming the grantee, the endpoint, the reason, and two remedies. |
ExternallyManaged | The library writes only the endpoint's resource-policy statement (if the endpoint is owned) and adds an info annotation. You are responsible for the caller's identity policy. |
ExternallyManaged does not mean permissions are configured
Selecting ExternallyManaged does not grant anything on the caller's side. It records the resource-policy statement and annotates that Invocation by {principal} requires an identity policy in the caller's account allowing lambda:InvokeWebFunctionEndpoint on {endpoint ARN}. Invocation fails until that policy exists.
Why GrantInvoke rejects certain principals¶
To avoid confused-deputy grants and false confidence, GrantInvoke fails synthesis for:
- Service principals β bare or conditioned. An unconditioned service-principal statement would let, for example, any distribution in any account with an OAC origin for this endpoint invoke it. Use
GrantCloudFrontInvoke(distributionArn)for CloudFront, or an explicit condition-scopedAddToResourcePolicystatement for any other service, including the scoping condition that service documents.GrantInvokecannot know which condition keys a service supplies, so it neither addsaws:SourceAccountnor accepts arbitrary conditions as proof of scoping. - Account principals (
AccountPrincipal,AccountRootPrincipal, and any principal whose literal AWS principal is an account-root ARN), including the same account. An account-root resource statement authorizes no specific caller by itself, so it is never treated as a completeAutomaticgrant. UnderExternallyManagedan explicitly requested account principal can be recorded in the owned endpoint's resource policy, but that is only half the picture β caller-side authorization is still not configured, and the result must not be described as a working grant. - Wildcard principals (conditioned or not) β
AnyPrincipal,StarPrincipal,OrganizationPrincipal,*. Conditioned wildcards are supported only through an explicitAddToResourcePolicystatement (see below).AddToResourcePolicyonly checks that an Allow wildcard statement has conditions β it does not verify that those conditions actually restrict access. You own the condition semantics: a condition such asaws:PrincipalOrgIDcan meaningfully restrict who may invoke, but the mere presence of a condition proves nothing, so design and validate the condition yourself. PrincipalWithConditionswrapping any principal, under either caller policy. Grant the underlying principal directly, or useAddToResourcePolicy.Role.WithoutPolicyUpdates()and every imported role as an identity-policy target β they are not concrete owned mutable identities, so the library falls back to the resource policy on an owned endpoint, or throws on an imported one.
If a case cannot be safely configured on either side, GrantInvoke throws. A false negative (requiring you to act) is always preferred over a false positive.
Resource-Policy Ownership¶
A Web Function has exactly one resource policy, attached to the function ARN, and the construct owns it. Every contribution flows through that single policy:
- The construct exposes no public "add to function policy" entry point. The only resource-policy entry points are on the endpoint:
GrantInvoke,GrantCloudFrontInvoke, andAddToResourcePolicy. endpoint.AddToResourcePolicy(statement)requiresstatement.Resourcesto be exactly that endpoint's ARN; anything else throws anArgumentException.- Statements are validated when added (a Deny is unrestricted; an Allow must not use
NotPrincipal; an Allow naming a wildcard or service principal must carry conditions), copied, frozen, and validated as part of the candidate policy document. A rejected statement leaves no partial state. - Identical statements are deduplicated at synthesis.
- Multiple distributions or endpoints never create competing policy resources.
Because the policy is a single PUT-replace resource, an out-of-band aws lambda-web put-resource-policy is overwritten the next time the policy changes, and AWS warns against combining it with AWS::Lambda::Permission on the same function. Keep this function's invocation authorization in this construct.
Imported Endpoints¶
Use WebFunctionEndpointConstruct.FromEndpointAttributes to reference an endpoint owned by another stack or app:
var shared = WebFunctionEndpointConstruct.FromEndpointAttributes(this, "shared", new WebFunctionEndpointAttributes
{
EndpointArn = "arn:aws:lambda:us-east-1:111122223333:web-function/shared/endpoint/default",
DomainName = "abc123.lambda-web.us-east-1.on.aws",
AuthType = WebFunctionAuthType.IamAuth,
});
An imported endpoint owns nothing. Its GrantCloudFrontInvoke and any grant that would need its resource policy throw β the resource policy is owned externally. GrantInvoke with CallerPolicy = ExternallyManaged also throws on an imported endpoint, because there is no owned resource policy to receive the statement; configure the caller's access in the endpoint owner's policy instead. It is usable as a CloudFront origin with WebFunctionOriginPermissions.ExternallyManaged, and it writes identity policies only for an owned mutable caller whose account is literally the same as the endpoint's.
- Literal
EndpointArnvalues are validated againstarn:{partition}:lambda:{region}:{12-digit account}:web-function/{function}/endpoint/{endpoint}; literalDomainNamemust be non-empty; literalAccountmust be 12 digits and agree with a literal ARN's account. - Account (
WebFunctionEndpointAttributes.Account) is required whenEndpointArnis a token and you want identity grants: the endpoint's account = the literal ARN account, else the literalAccount, otherwise it is not established andGrantInvokethrows for every grantee. A token-ARN endpoint can still be used as a CloudFront origin withExternallyManagedpermissions.
CloudFront OAC Integration¶
WebFunctionOrigin is a static factory that returns a CloudFront IOrigin, analogous to CDK's FunctionUrlOrigin.
var origin = WebFunctionOrigin.WithOriginAccessControl(web.DefaultEndpoint, new WebFunctionOriginWithOacProps
{
CustomHeaders = new Dictionary<string, string> { ["x-origin-verify"] = secret },
});
var distribution = new Distribution(this, "distribution", new DistributionProps
{
DefaultBehavior = new BehaviorOptions
{
Origin = origin,
ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
AllowedMethods = AllowedMethods.ALLOW_ALL,
CachePolicy = CachePolicy.CACHING_DISABLED,
OriginRequestPolicy = OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
},
AdditionalBehaviors = new Dictionary<string, IBehaviorOptions>
{
["/_next/static/*"] = new BehaviorOptions { Origin = origin, CachePolicy = CachePolicy.CACHING_OPTIMIZED },
},
DomainNames = ["app.example.com"],
Certificate = certificate,
});
WithOriginAccessControl(endpoint, props)requires anIamAuthendpoint. When bound to a distribution it creates oneFunctionUrlOriginAccessControl(origin typelambda,always-amz-auth,sigv4for HomeRegion/PerRegion andsigv4afor MultiRegion) and adds the resource-policy statementcloudfront.amazonaws.com β lambda:InvokeWebFunctionEndpointon the endpoint ARN withStringEquals aws:SourceArn = arn:aws:cloudfront::{account}:distribution/{id}.WithoutOriginAccessControl(endpoint, props)requires anApplicationManagedendpoint; the endpoint remains directly reachable and CloudFront adds no access control.- Passing an
ApplicationManagedendpoint to the OAC factory, or anIamAuthendpoint to the non-OAC factory, throws anInvalidOperationExceptionat the factory call. - Automatic permissions (the default) require the distribution and the Web Function to be in the same stack; otherwise synthesis fails with a dependency-cycle explanation. For a cross-stack distribution, use
Permissions = WebFunctionOriginPermissions.ExternallyManagedand grant the distribution ARN viaendpoint.GrantCloudFrontInvoke(...)in the owner's stack (a two-phase rollout). - The same origin object reused on several behaviors of one distribution binds once, producing one OAC and one policy statement.
- A supplied
OriginAccessControlis used unchanged and not inspected; the consumer is responsible for it beinglambda/always-amz-auth/correct protocol.
Caching and header guidance¶
- Never forward the viewer
Hostheader. UseOriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, notALL_VIEWER. ForwardingHostmakes the endpoint return404. - Dynamic/personalized routes should use
CACHING_DISABLED. - For GET/HEAD, CloudFront drops the viewer
Authorizationheader unless it is part of the cache policy; prefer cookies or custom headers, and addAuthorizationto a cache policy only when you truly need it. - Host-dependent applications (multi-tenant hosts) must recover the viewer host through a CloudFront Function that copies it into a custom header, and trust that header only after an origin-verification check.
- Content-hashed static assets (
/_next/static/*, Blazor fingerprinted/_framework/*files,/_content/*) should useCACHING_OPTIMIZED. - Lambda@Edge origin-request/origin-response triggers are rejected by CloudFront on behaviors that use a Web Function origin with OAC.
- OAC is not supported for global endpoints that include opt-in regions (requests get
403). - OAC takes effect only after the distribution deploys (and, for MultiRegion, after the policy replicates); brief
403s in between are expected.
Grant Behavior Reference¶
The table below summarizes GrantInvoke behavior for an owned endpoint (an endpoint on a function in this app). An imported endpoint is stricter: it owns no resource policy, so only an owned mutable caller whose account is literally the same as the endpoint's can be granted (via an identity policy), and every other case throws.
| Grantee | Automatic (default) | ExternallyManaged |
|---|---|---|
Owned mutable identity (Role, User, Group, LazyRole created in this app) β same stack, or different stacks with the same literal account | Complete β identity policy written | Partial by contract β only the resource-policy statement is written; you configure the caller's identity policy |
Role.FromRoleArn (imported) whose literal account equals the endpoint's | Complete β the owned endpoint's resource policy is used | Partial by contract |
Role.WithoutPolicyUpdates() (owned, but not a mutable identity) | Complete via the resource policy (no identity policy written) | Partial by contract |
| Account principal / account-root principal | Synthesis error | Partial by contract β the account principal may appear in the endpoint's resource policy, but caller-side authorization is not configured |
Service principal (for example cloudfront.amazonaws.com) | Synthesis error β use GrantCloudFrontInvoke for CloudFront, or a condition-scoped AddToResourcePolicy | Synthesis error |
Wildcard principal (AnyPrincipal, StarPrincipal, *) | Synthesis error | Synthesis error β use an explicit conditioned AddToResourcePolicy |
PrincipalWithConditions wrapper | Synthesis error | Synthesis error |
| Cross-account caller (different literal account) | Synthesis error | Partial by contract |
| Caller whose account cannot be established (unresolved) | Synthesis error | Partial by contract |
| Any grantee on an imported endpoint | Treated as owned-mutable-same-account only; otherwise synthesis error | Synthesis error (no owned resource policy) |
"Partial by contract" means the library writes the resource-policy statement (on an owned endpoint) and annotates what the caller must do; it never claims the caller side is configured.
Signing Requests¶
A direct IAM invocation of an IamAuth endpoint must be signed with AWS Signature Version 4:
- Service name:
lambda. - Payload hash: send
x-amz-content-sha256: UNSIGNED-PAYLOAD(chunked payload signing is not supported). - Protocol:
HomeRegionandPerRegionendpoints use SigV4 signed for the endpoint's Region;MultiRegionendpoints use SigV4A with the signing region set to*. - The construct (and CloudFront, when not using OAC) does not sign requests on your behalf β the calling SDK or your BFF signs itself.
CloudFront OAC signing is different: with always-amz-auth, CloudFront signs the origin request in its own dedicated header and passes the viewer's Authorization header through unchanged. Lambda strips its own SigV4 Authorization (and X-Amz-Security-Token) before forwarding, so application code sees the viewer's header, not the caller's signature.
Resource Replacement and Update Behavior¶
| Change | Effect |
|---|---|
| Code key/version, environment variables, timeout, execution role, logging, description | Creates a new revision; endpoints move to it without interruption; the previous revision is removed during cleanup. |
MaxEnvironments, RateLimit, endpoint Description | Updated in place (no interruption). Omitting the option keeps the current value; clearing it removes the setting. |
Endpoint AuthType | No interruption, but switching ApplicationManaged β IamAuth while introducing an OAC can return transient 403s until the distribution deploys. |
EndpointName, EndpointType, Regions | Replacement. Reusing a fixed name fails with "already exists" and the endpoint domain changes β add a new endpoint, move consumers to it, then remove the old one. |
FunctionName | Replacement of everything. Fails while the old name still exists; endpoint domains change. |
| Distribution replacement | The distribution ID changes; the policy statement is updated in the same deployment (no interruption), though the new distribution may return 403 until the policy update propagates. |
| Resource-policy statement changes | The PolicyDocument is updated in place (no interruption). |
A deployment that fails is rolled back by CloudFormation to the previous revision. These behaviors follow the CloudFormation resource model in the approved design; anything not listed here should be treated as unverified.
Public Testing Helpers¶
The LayeredCraft.Cdk.Constructs.Testing namespace ships fluent helpers for synthesized templates:
WebFunctionConstructAssertionsextension methods onTemplate:ShouldHaveWebFunction,ShouldHaveRevision,ShouldHaveRevisionCode,ShouldHaveEndpoint,ShouldHaveEndpointScaling,ShouldHaveInvokeGrant,ShouldHaveSingleResourcePolicy,ShouldHaveNoResourcePolicy,ShouldAllowCloudFront,ShouldAllowResourcePolicyPrincipal,ShouldHaveWebFunctionOac,ShouldHaveCodeBucketReadStatement,ShouldHaveWebFunctionLogGroup,ShouldHaveCompanionLogGroup, andShouldHaveRevisionDependingOnLogGroups. The function, endpoint, and policy assertions match by function and endpoint names (not logical IDs) and requireEffect = Allowwith the expected action and the exact endpoint resource, so a Deny or an unrelated action never satisfies them. The log-group assertions match by log-group name and exactRetentionInDays;ShouldHaveRevisionDependingOnLogGroupsresolves names to logical IDs through the template.WebFunctionConstructPropsBuilderviaCdkTestHelper.CreateWebFunctionPropsBuilder():WithFunctionName,WithCode,WithTestArtifact,WithIamAuthEndpoint,WithApplicationManagedEndpoint,WithEndpoint,WithEnvironmentVariable,WithEntryPoint,WithTimeout,WithMaxConcurrencyPerEnvironment,WithRole,WithLogs, thenBuild().Build()throws without code or an endpoint.
Note that ShouldHaveLogGroup belongs to LambdaFunctionConstructAssertions; use ShouldHaveWebFunctionLogGroup for Web Functions.
var template = Template.FromStack(stack);
template.ShouldHaveWebFunction("acme-web");
template.ShouldHaveEndpoint("acme-web", "default", WebFunctionAuthType.IamAuth);
template.ShouldAllowCloudFront("acme-web", "default");
template.ShouldHaveWebFunctionLogGroup("/aws/lambda/web/acme-web", 14);
template.ShouldHaveCompanionLogGroup("/aws/lambda/web/acme-web", 3);
template.ShouldHaveRevisionDependingOnLogGroups(
"acme-web",
"/aws/lambda/web/acme-web",
"/aws/lambda/web/acme-web.local");
Workload Recipes¶
Detailed recipes for Next.js, ASP.NET Core (Node.js execve shim), and Blazor Static SSR are in the Web Function Workloads page. They show how to build each artifact and wire CloudFront, and they are explicit about what synthesis tests verify versus what requires a real AWS deployment.
Known Limitations and Operational Guidance¶
- S3 versioning via escape hatches is not detected. The owned-bucket inspection only reads literal values in the rendered L2 properties. Versioning enabled through a property override (
CfnBucket) is not detected; reference such a bucket by name (Bucket.FromBucketName) withExternallyManagedinstead. - Tokenized configuration is skipped, never rejected. A token versioning status, encryption algorithm, or key ID can be shown neither compliant nor non-compliant, so validation skips it.
- Log-group switching. Moving an existing function from
WebFunctionLogs.ServiceDefault()toCreate()β or adopting a managed companion where Lambda already created{name}.localβ can fail withAlreadyExists, because Lambda may have auto-created the group. See Upgrading an existing deployment for the supported remedies. - Service-created default log groups outlive the stack. With
WebFunctionLogs.ServiceDefault()(or aUsecompanion the construct does not manage) the construct creates noAWS::Logs::LogGroup; the Lambda service creates the group on first invocation. Such groups are not part of the stack, have no expiry, and are not removed by CloudFormation on teardown, so they keep the invocation history after the stack is deleted. CDK-managedCreate()groups are stack-owned and follow theirRemovalPolicy. In disposable sandboxes, delete leftover service-created groups explicitly by exact name after destroying the stack. Do not routinely delete production log groups. - IAM policy propagation. The revision depends on the execution role's
CfnRoleonly, never the role's default policy, so permissions granted to the role may not have propagated when the revision first activates. This avoids CloudFormation cycles for mutually-referencing functions. - CloudFront origin-binding exceptions across jsii. Validation that runs inside the CDK
IOrigin.Bindpath (and insideHttpOriginconstruction) surfaces in .NET as aSystem.Exceptioncarrying CDK's message, whereas factory-time checks throwInvalidOperationException. - F3006 resource-type warnings. CDK 2.274.0 emits
F3006"Unknown resource typeAWS::Lambda::WebFunction*" validation warnings. They are warnings by default and become synthesis errors when@aws-cdk/core:validateAgainstDefaultRulesis enabled; acknowledge withCloudFormation-Validate::F3006. - OAC naming. The auto-created OAC name is derived from the distribution's origin scope. OAC names are unique per AWS account across Regions, so the collision risk is identical names within the same account β not across different accounts. If you deploy the same distribution shape repeatedly in one account, verify the generated naming still meets your conventions.
- Origin request policy. Use
ALL_VIEWER_EXCEPT_HOST_HEADER, neverALL_VIEWER(see Caching and header guidance). - Customer-managed revision keys.
KmsKeyArnis not supported in v1. The only workaround is an unsupported escape hatch β((CfnWebFunctionRevision)web.Node.FindChild("Revision")).KmsKeyArn = key.KeyArnplus the key-policy grants you write. - Sandbox validation. Runtime behavior (actual invocation, execve shims, readiness, CloudFront end-to-end) is validated only by a real deployment in a sandbox account; synthesis tests do not prove runtime support.
Validation Errors¶
The construct fails at synthesis with ArgumentException, ArgumentOutOfRangeException, or InvalidOperationException, following repository style. Literal values are validated; tokens (Token.IsUnresolved) are skipped, never rejected. Notable cases:
- OAC origin for an
ApplicationManagedendpoint, or non-OAC origin for anIamAuthendpoint βInvalidOperationExceptionat the origin factory. - Automatic OAC permissions with an imported endpoint or a cross-stack distribution β
InvalidOperationExceptionat bind. GrantInvoke/GrantCloudFrontInvokeon anApplicationManagedendpoint, or a resource-policy grant on an imported endpoint βInvalidOperationException.- An
Automaticgrant that cannot be fully configured βInvalidOperationExceptionnaming the grantee, endpoint, reason, and remedies. AddToResourcePolicywith a resource that is not exactly the endpoint ARN, or an unconditioned wildcard/service-principal Allow, orNotPrincipalβArgumentException.- A props-builder
Build()without code or endpoint βInvalidOperationException.
Testing¶
See the Testing Guide for the test helpers and patterns shared across constructs.
Examples¶
See the Examples page and the Web Function Workloads page.