Skip to content

feat(lambda): add WebFunction and NetworkConnector L2 constructs - #39210

Open
licjun wants to merge 75 commits into
aws:mainfrom
licjun:feat_lambda_webfunction
Open

licjun wants to merge 75 commits into
aws:mainfrom
licjun:feat_lambda_webfunction

Conversation

@licjun

@licjun licjun commented Oct 13, 2026 •

Copy link
Copy Markdown

Issue # (if applicable)

N/A

Reason for this change

Lambda WebFunction launched on 2026-10-12. Today it can only be defined through its
three L1 resources — AWS::Lambda::WebFunction, AWS::Lambda::WebFunctionRevision
and AWS::Lambda::WebFunctionEndpoint — which the caller has to wire together
themselves, including the dependency order between them and the FunctionName
each of them repeats.

Code is the harder part. A WebFunction reads its code from S3 at every sandbox
start, using the Lambda service principal through a bucket resource policy, not
the caller's credentials. The bucket therefore has to have versioning enabled (a
revision pins one object version for its lifetime), SSE-S3 encryption (the
AWS-managed KMS key cannot grant the service principal) and that read policy. The
CDK bootstrap bucket satisfies none of these, so lambda.Code.fromAsset cannot be
used and local code needs a staging step.

VPC egress works differently from lambda.Function: a WebFunction does not take
subnets and security groups, it references an AWS::Lambda::NetworkConnector that
owns the network interfaces, so they are provisioned once with the connector rather
than per function.

Description of changes

WebFunction creates the function, one revision and one endpoint, and sets the
dependencies and shared FunctionName between them. It exposes domainName,
endpointArn, revisionArn and revisionId, implements IGrantable, offers
grants.invoke(), and creates an execution role if none is given. authType is
required with no default, so making an endpoint public is always explicit.

WebFunctionCode has three factories:

  • fromBucket — code already in a compliant bucket; the bucket is validated, not
    modified.
  • fromAssetToManagedBucket — a local asset staged into a per-stack bucket CDK
    creates with versioning, SSE-S3 and the service-principal read policy.
  • fromAssetToProvidedBucket — a local asset staged into the caller's bucket, which
    is validated for versioning and encryption.

Staging uses a new internal custom resource that copies one object server-side with
CopyObject and returns the VersionId it wrote, which the revision pins. This is
why it is not BucketDeployment: that resource does not report the written version,
so getting one would need a second custom resource calling headObject, which
returns whichever version is current when it runs rather than the one just written.
An update writes a new object version and leaves the old one in place; the old version
is deleted only when the copy it belongs to is removed, in the cleanup phase, and kept
if the revision reading it is retained. Because the copy is server-side, the object's
bytes never pass through Lambda — no AWS CLI layer, no /tmp ceiling.

NetworkConnector wraps AWS::Lambda::NetworkConnector. Its configuration is
chosen through NetworkConnectorConfiguration.vpcEgress({...}), which takes a vpc
plus an optional vpcSubnets selection and the security groups. By default the
construct creates the operator role the service assumes to manage the interfaces and
attaches the AWSLambdaNetworkConnectorOperatorPolicy managed policy; a role defined
in the same app gets that policy attached too, and an imported one gets a warning
since it cannot be modified.

Every update to a connector produces a new immutable version, so
WebFunctionProps.egressNetworkConnector pins the revision to the connector's
latestVersionArn. A connector in the same stack is therefore replaced-and-moved
as one deployment; a connector in another stack is rejected at synthesis, because
that ARN changes on every update and CloudFormation will not let an Export change
while another stack imports it. NetworkConnector.fromNetworkConnectorVersionArn
is the way to pin a fixed version, including across stacks.

Describe any new or updated permissions being added

  • The code bucket grants s3:GetObject and s3:GetObjectVersion to
    lambda.amazonaws.com, conditioned on aws:SourceAccount and aws:SourceArn. For
    fromBucket and a provided bucket, it is scoped to the function's code and its
    revisions; for the managed bucket, one statement covers web-function/* and any
    WebFunction revision in the account.
  • A caller-supplied KMS key is granted to the same service principal under the same
    conditions, so the service can decrypt the code and environment variables.
  • The copy custom resource's role gets s3:GetObject and s3:GetObjectVersion on the
    source bucket's objects, and put and delete on the destination key pattern.
  • A WebFunction's default execution role gets AWSLambdaBasicExecutionRole.
  • grants.invoke() grants lambda:InvokeWebFunctionEndpoint on the endpoint ARN.
  • A NetworkConnector's default operator role is created with
    AWSLambdaNetworkConnectorOperatorPolicy and trusts lambda.amazonaws.com.

Description of how you validated changes

Unit tests for both constructs, the code factories and the copy custom resource,
including its handler.

Five integ tests, each with a snapshot generated by a real deployment in
us-west-2: a minimal WebFunction, one setting every supported prop, one staging a
local asset into the managed bucket, a standalone NetworkConnector, and a
WebFunction with egress through a connector. The WebFunction tests assert against
the deployed endpoint. Under IAM_AUTH an unsigned request gets 403, which
confirms the endpoint is live and enforcing auth. The connector test uses an
APPLICATION_MANAGED endpoint and expects 200 with an object the handler can only
read through the VPC's S3 gateway endpoint, so it also shows the handler runs and its
egress goes through the connector.

Both constructs were also exercised by hand in a bug bash with several engineers,
and the findings from it are fixed in this change.

Checklist


awslint exclusion

props-physical-name:aws-cdk-lib.aws_lambda.WebFunctionEndpointProps is excluded on
purpose: the endpoint's physical name is endpointName, the service's term
(AWS::Lambda::WebFunctionEndpoint.EndpointName), rather than webFunctionEndpointName.

Not in this PR: weighted traffic

WebFunctionTraffic has only single(). A WebFunction keeps one revision, so there is no
second revision to split traffic with; weights need addressable, retained revisions, which
can be added later together with a WebFunctionTraffic.weighted() factory without changing
single() or the traffic prop. Until then, use endpoint.resource to set RevisionWeights.

Integ snapshot changes

integ.webfunction-network-connector and integ.webfunction-staged-asset give their
Custom::S3ObjectCopy resources new logical ids: a copy's id now depends only on the copy
function, and its key is computed by the handler from the content and the revision's
configuration. Both were regenerated by a deployment.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

Adds `WebFunction`, which wraps the three L1 resources a Lambda
WebFunction needs — the function, an immutable revision and an
endpoint — into one construct, and `NetworkConnector`, which gives a
WebFunction egress into a VPC through connector-managed network
interfaces.

- `WebFunctionCode` takes code from a bucket the caller already has, or
  stages a local asset into a CDK-managed or caller-provided bucket. The
  bucket's requirements (versioning, SSE-S3, a Lambda service-principal
  read policy) are either configured by CDK or checked, since the
  service reads the code at every sandbox start.
- Staging copies one object server-side through a custom resource that
  reports the version it wrote, so a revision pins exactly the version
  of the code it was created with.
- `NetworkConnector` creates its operator role by default and attaches
  the `AWSLambdaNetworkConnectorOperatorPolicy` managed policy; a
  WebFunction pins its revision to the connector's latest version.

Includes unit tests, five integ tests with deployed snapshots, and
README sections for both constructs.

The CloudFormation registry schemas for these resources are vendored
under `temporary-schemas` until the published service spec carries the
revision's `EgressNetworkConnectorArn` and the connector's `WebFunction`
compute type.
@github-actions github-actions Bot added the p2 label Oct 13, 2026
@github-actions github-actions Bot added the beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK label Oct 13, 2026
@aws-cdk-automation
aws-cdk-automation requested a review from a team October 13, 2026 17:38
@github-actions

Copy link
Copy Markdown
Contributor

👋 It looks like your PR description follows the template but is missing a valid issue number in the first section.

PRs without a linked issue will receive lower priority for review and merging. Please update the description to include a reference like Closes #123. If no existing issue matches your change, create one first.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Experimental Feature: This security report is currently in experimental phase. Results may include false positives and the rules are being actively refined.
This security report is NOT a review blocker. Please try merge from main to avoid findings unrelated to the PR.
To suppress a specific rule, see Suppressing Rules.


TestsPassed ✅Skipped ⚠️Failed
Security Guardian Results276 ran271 passed5 skipped
TestResult
No test annotations available

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Experimental Feature: This security report is currently in experimental phase. Results may include false positives and the rules are being actively refined.
This security report is NOT a review blocker. Please try merge from main to avoid findings unrelated to the PR.
To suppress a specific rule, see Suppressing Rules.


TestsPassed ✅Skipped ⚠️Failed
Security Guardian Results with resolved templates276 ran271 passed5 skipped
TestResult
No test annotations available

@maintainer-for-aws

maintainer-for-aws Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Automated review

A maintainer will still review this — treat the notes below as a starting point.

📌 Commit 51ce842

This PR adds net-new L2 constructs for AWS Lambda's WebFunction family — WebFunction, WebFunctionEndpoint, NetworkConnector, and supporting WebFunctionCode/WebFunctionRuntime/WebFunctionTraffic value types plus an internal Custom::S3ObjectCopy resource — to aws-cdk-lib. It collapses the three WebFunction L1 resources and the network connector into ergonomic constructs and solves S3 code staging and VPC egress. The modified files (index.ts, custom-resources config.ts, the READMEs, and awslint.json) are purely additive, so nothing changes for existing stacks.

The change is in good shape overall: the abstraction and layering are sound, token handling is carefully guarded throughout, the public surface is JSII-clean, IAM grants and resource policies are scoped and secure-by-default (public exposure is always explicit via a required auth choice), and test coverage — unit tests plus five integration tests with real deploy-time assertions — is thorough. No blocking issues were found. The findings are design-polish and documentation items: a missing CloudWatch metric* affordance on the new L2s, a few interface/convention gaps (the endpoint's missing static type guard, IWebFunction not extending the shared resource-policy interface), prop-shape observations where mutually-constrained flat props could be modeled polymorphically, an inaccurate @default on endpointName for the addEndpoint() path, and an undocumented awslint suppression. Each can be addressed additively without a breaking change.

🔴 0 blocking · 🟡 7 recommended · ⚪ 3 optional

Files with findings (5)
File Findings
packages/aws-cdk-lib/aws-lambda/lib/network-connector.ts 🟡 1
packages/aws-cdk-lib/aws-lambda/lib/webfunction-endpoint.ts 🟡 3 ⚪ 1
packages/aws-cdk-lib/aws-lambda/lib/webfunction.ts 🟡 2 ⚪ 1
packages/aws-cdk-lib/awslint.json 🟡 1
packages/aws-cdk-lib/aws-lambda/lib/webfunction-runtime.ts ⚪ 1

Generated automatically. React 👍 or 👎 to tell us whether this review helped, so we can improve these reviews.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review summary comment for the overview; the notes below are inline.

Comment thread packages/aws-cdk-lib/aws-lambda/lib/webfunction.ts Outdated
Comment thread packages/aws-cdk-lib/aws-lambda/lib/webfunction.ts Outdated
@aws-cdk-automation aws-cdk-automation added the pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. label Oct 13, 2026
The PR build failed on three awslint errors in the new constructs.

- no-grants: L2s may not add new grantXxx() methods. grantInvoke moves
  to a companion WebFunctionGrants class, reached as
  `fn.grants.invoke(grantee)`, following the other *Grants classes.
  Like them it takes the L1 reference interface, here
  IWebFunctionEndpointRef, since invoke is granted on the endpoint ARN.
  WebFunctionBase implements that interface so both a WebFunction
  defined in the app and an imported one get `grants`. grantInvoke is
  removed rather than kept as a deprecated alias, since it has not been
  released. The grant itself is unchanged.
- from-signature: rename fromNetworkConnectorVersionArn's parameter to
  networkConnectorVersionArn.
- prefer-ref-interface: egressNetworkConnector keeps the L2
  INetworkConnector, with the rule disabled and the reason documented.
  The revision pins to latestVersionArn, which INetworkConnectorRef
  does not carry.
…ndpoint

Review comment r4262093418: a revision that specifies a network
connector only supports a HomeRegion endpoint (CloudFormation docs for
WebFunctionRevision ServiceConfig.EgressNetworkConnectorArn), but the
construct accepted egressNetworkConnector with MULTI_REGION or
PER_REGION, so the mistake surfaced only at deployment. It is now
rejected at synthesis, alongside the other endpointType checks.
Review comment r4262093428: the region groups are literal lists, so a
pinned aws-cdk-lib keeps the regions that existed when it was built,
and the "17 regions" count on ALL_DEFAULT_REGIONS goes stale as the
service adds regions. The class docs now say the groups are fixed as of
the library version and to list regions explicitly to cover a newly
launched one, and the count is dropped. The groups themselves stay:
their shape was accepted in review.
Review comment (aws-cdk-private#433, r4270167013): WebFunction should
create a CDK-managed log group by default, unconditionally and not
behind a feature flag.

Without a logGroup, the revision set no LoggingConfig, and the service
created /aws/lambda/web/<function> at runtime with the execution role.
That group is outside CloudFormation: it never expires, CDK cannot set
its retention or encryption, and it outlives `cdk destroy`. Deployed in
toby to confirm: the group appeared on the first request, and after
the stack was deleted it was left behind.

The construct now creates /aws/lambda/web/<function> and always passes
the log group to the revision. The name matches the service default
(verified: the service writes into a group CDK created under that name),
and referencing it makes the revision depend on it, so CloudFormation
deletes the function before the log group. A single group in the home
Region is the right model for multi-Region endpoints too: logs are
written to <group>.local in each serving Region and copied into this
group (verified with a PER_REGION endpoint, requests sent to us-east-1
only). The .local groups are created by the service and stay unmanaged.

Other LogGroup properties keep their defaults, as for Function with
useCdkManagedLogGroup.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review summary comment for the overview; the notes below are inline.

Comment thread packages/aws-cdk-lib/aws-lambda/lib/webfunction.ts
Comment thread packages/aws-cdk-lib/aws-lambda/lib/webfunction-endpoint.ts
Ref on AWS::Lambda::WebFunction returns the ARN, so the override that
returned the L1's ref as functionName gave an in-app function's ARN
where an imported one gave its name (r4405940444).

webFunctionRef.functionName is now webFunctionName, and the endpoint
takes webFunctionRef.functionArn as its FunctionName, which accepts an
ARN: a GetAtt on the function, so the endpoint still depends on it.
Regenerated by deploying all four tests after the endpoint started
taking the function's ARN as its FunctionName.
The previous snapshot update committed two asset zips as plain files
rather than LFS pointers. Their content is unchanged, so the pointers
are the ones they had before.
IWebFunction declared addToResourcePolicy() itself instead of extending
iam.IResourceWithPolicyV2, so an IWebFunction could not be passed to
APIs typed to it, unlike IWebFunctionEndpoint (r4406339134).
* regions except X" built that way does not include a newly launched region either. To cover
* one, upgrade `aws-cdk-lib` or list the regions explicitly; explicit lists are unaffected.
*/
export class WebFunctionRegions {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These groups expand into the endpoint's Regions, which is create-only, and into the default prod-<hash> name, so changing a group's contents or order replaces the endpoint and changes its domainName. That leaves two options once Lambda launches in more Regions. Updating a group gives every app that uses it a new endpoint URL on a routine aws-cdk-lib upgrade. Freezing the groups keeps ALL_DEFAULT_REGIONS in stable API under a name that is no longer true, along with SPARSE_GLOBAL and MAJOR_REGIONS as fixed picks.

Could they leave this PR instead? Without groups, WebFunctionReplicaRegions and its excluded list aren't needed either, so multiRegion() and perRegion() could take the Regions directly:

WebFunctionEndpointLocation.multiRegion(['us-east-1', 'eu-west-1']);

Groups can be added later without a breaking change. Removing them after release would be one.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, removed in 00ac84a. WebFunctionRegions and WebFunctionReplicaRegions are gone, and multiRegion() / perRegion() take the Regions directly. Groups can come back later as an addition if there's a way to make them stable.

// are seen. The auto-delete resource is retained with the bucket, so it is never sent the
// Delete that would empty it.
const autoDelete = bucket.node.tryFindChild('AutoDeleteObjectsCustomResource')?.node.defaultChild;
for (const resource of [bucket.node.defaultChild, autoDelete]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This retains the bucket and the auto-delete resource but not bucket.policy, which holds the only lambda.amazonaws.com read grant. After stack deletion with a retained revision, the bucket and object survive and the policy is deleted, so the revision fails at its next sandbox start, which is the case this is meant to cover. Adding bucket.policy?.node.defaultChild to the list, and asserting AWS::S3::BucketPolicy in the retention tests, would close it. A provided bucket has the same gap, because its CDK-created policy defaults to Delete.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, fixed in 6f5bb8b. The managed bucket's policy is now retained with the bucket, and a provided bucket's policy is retained while any revision using it is; otherwise both keep their own removal policy. The retention tests now check AWS::S3::BucketPolicy for both.

}

const regions = location.replicaRegions?._toRegionList();
if (regions !== undefined && !Token.isUnresolved(regions) && regions.length === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only an empty list is rejected. CloudFormation requires at least one Region other than the home Region (Regions: "You can't specify only the home Region"), so multiRegion(WebFunctionReplicaRegions.of(['us-east-1'])) in a us-east-1 stack passes synth and fails at deploy.

The default name also hashes the raw list (private/webfunction-util.ts:16), so reordering it, or adding or removing the home Region, renames and replaces an endpoint that is otherwise the same. Could the home Region be dropped and the list sorted before hashing and rendering, with an error when nothing is left? Changing the hash inputs after release would rename every default regional endpoint.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 00ac84a. The list is now treated as a set: duplicates and the stack's own Region are dropped and the rest sorted before both rendering and the default name, and a list with no other Region fails at synthesis. In an environment-agnostic stack the Region isn't known, so only the dedupe and sort apply there.

Comment on lines +28 to +30
// MicroVm rather than WebFunction: the published registry schema accepts only MicroVm
// today, so this is the compute type a real deployment can exercise. The construct
// code under test is the same for both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: this comment is out of date now that the vendored schema is gone and integ.webfunction-network-connector.ts deploys a WEB_FUNCTION connector. Keeping MICRO_VM here still makes sense, since it's the only deploy of that compute type, but the comment could give that as the reason instead. An awsApiCall on the connector's state would also make the test assert something beyond a successful deploy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated the comment in 81e8a92. On the state check: awsApiCall needs the command in the AWS SDK, and the latest public @aws-sdk/client-lambda (3.1159.0) has no NetworkConnector commands yet, so it would fail at deploy. I'll add it once the SDK has GetNetworkConnector. The WEB_FUNCTION connector is exercised end to end in integ.webfunction-network-connector, where the handler reaches an S3 object through it.

The bucket policy holds the service's read grant, so a retained revision
whose bucket outlived the stack could no longer read its code. The
managed bucket's policy, and a provided bucket's policy, are now retained
while any revision using them is (r4406775537).
Remove WebFunctionRegions and WebFunctionReplicaRegions. A group's
contents can change with an aws-cdk-lib upgrade, and the Regions are
create-only and feed the default endpoint name, so an upgrade would
replace endpoints and change their domain names (r4406763257).
multiRegion() and perRegion() now take the Regions directly.

The list is treated as a set: duplicates and the stack's own Region are
dropped and the rest sorted before rendering and naming, and a list with
no other Region is rejected at synthesis instead of at deploy
(r4406841770).
The vendored schema is gone; the test keeps MicroVm because it is the
only one that deploys that compute type (r4406898201).

@piotrekwitkowski piotrekwitkowski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seven inline comments below from another review pass, posted here so more people can weigh in.

One more that has no single line. No integ test deploys a resource policy, the IAM_AUTH success path, a MULTI_REGION or PER_REGION endpoint, or addEndpoint(). So the policy this PR renders, grants.invoke() with a non-identity principal, and the regional endpoint naming haven't been accepted by the service in a test. An integ with a helper Lambda that signs requests (SigV4 plus X-Amz-Content-SHA256), granted through endpoint.grants.invoke() and asserting a 200, plus one MULTI_REGION addEndpoint(), would cover most of it.

cc @matboros

return { 'Fn::If': [args[0], fingerprint(args[1]), fingerprint(args[2])] };
case 'Fn::Select':
return { 'Fn::Select': [args[0], fingerprint(args[1])] };
default:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Literals inside any intrinsic other than the four handled above pass through unchanged, also when that intrinsic is nested inside a handled one. So a {{resolve:...}} inside Fn::Base64, Fn::ToJsonString, Fn::Select over Fn::Split, a Fn::FindInMap default, or an Fn::Base64 value in an Fn::Sub variable map still reaches the copy and fails the deploy, for example environment: { AUTH: Fn.base64(admin:${secret.unsafeUnwrap()}) }.

An allowlist would close the whole class. Keep only Ref, Fn::GetAtt, Fn::ImportValue and Fn::Sub placeholders, keep the Fn::If skeleton around them so conditional references stay conditional, and hash everything else. A synth-time error if {{resolve: is left in the result would make it fail closed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 5d28935. The fingerprint is an allowlist now: Ref, Fn::GetAtt and Fn::ImportValue are kept, Fn::Join, Fn::Sub and Fn::If keep their shape with their literals hashed, and any other intrinsic is replaced by the hash of its value. The references inside it go into a separate References list, under their Fn::If when conditional, so they still change the key without being evaluated in place (a list-valued one inside a Fn::Join would break it). Synthesis fails if {{resolve: is left. Tests cover the Fn::Base64, Fn::Select over Fn::Split, nested and Fn::Sub variable cases.

/**
* Whether a revision is retained, read when the template is resolved.
*/
function retentionOf(resource: cdk.CfnResource): () => { onReplace: boolean; any: boolean } {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

retentionOf reads cfnOptions and revisionConfiguration (:664) reads the L1 getters, so escape-hatch overrides on the revision are invisible to both. fn.resourceRevision.addOverride('DeletionPolicy', 'Retain') (and UpdateReplacePolicy) leaves the copy at Delete, so the next code change deletes the object the retained revision pins. addPropertyOverride('ServiceConfig.TimeoutSeconds', 60) replaces the revision without moving the key, so the old and new revisions share one object version again.

Deriving both from the resolved _toCloudFormation() output, minus BuildConfig.CodeConfig, the way function-hash.ts does for lambda.Function, would pick up both.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 5d28935: the copy's policies and its configuration are both read from the revision's rendered _toCloudFormation() output, minus BuildConfig.CodeConfig, so addOverride on the policies and addPropertyOverride on properties are seen. Tests for both.

/**
* The domain name of the endpoint: the host clients send requests to.
*
* For a `PER_REGION` endpoint this is the home Region's domain name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this confirmed for PER_REGION? The CloudFormation reference only documents DomainName as "The domain name assigned to the endpoint", and the per-Region hostnames are in RegionalEndpoints. No integ test deploys a PER_REGION endpoint, so this contract would freeze unverified. If it can't be checked before merge, I'd drop the sentence here and at :426.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, checked on a deployed endpoint: a PER_REGION endpoint in a us-west-2 stack returned the DomainName <id>.jcg.lambda-web.us-west-2.on.aws, the home Region's hostname, so I kept the sentence. The new integ.webfunction-endpoints (9c8aced) also deploys a PER_REGION endpoint and checks that its domainName answers (403 unsigned).

function retentionOf(resource: cdk.CfnResource): () => { onReplace: boolean; any: boolean } {
return () => {
const isRetain = (p?: cdk.CfnDeletionPolicy) =>
p === cdk.CfnDeletionPolicy.RETAIN || p === cdk.CfnDeletionPolicy.RETAIN_EXCEPT_ON_CREATE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

RETAIN_EXCEPT_ON_CREATE counts as retained here, so the copy, the managed bucket and the auto-delete resource get a plain Retain. If the first deployment fails, CloudFormation deletes the revision but keeps the bucket and its object, which is what RetainExceptOnCreate exists to avoid. Carrying the strongest policy across (Delete < RetainExceptOnCreate < Retain) instead of a boolean would keep it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 5d28935. The strongest policy is carried (Delete < RetainExceptOnCreate < Retain): a revision with RetainExceptOnCreate gives the copy, the managed bucket, its policy and the auto-delete resource a DeletionPolicy of RetainExceptOnCreate. UpdateReplacePolicy has no such value, so it is Retain wherever something is retained.

import { S3 } from '@aws-sdk/client-s3';
import { makeHandler } from '../../nodejs-entrypoint';

const s3 = new S3({});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This client uses the copy function's Region. A provided bucket in another Region, which the Lambda docs allow for code, then fails CopyObject and DeleteObject with PermanentRedirect. new S3({ followRegionRedirects: true }) would cover it, and a test with a bucket from Bucket.fromBucketAttributes({ region }) would guard it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 726be1d. I deployed a revision in us-west-2 whose code was in a us-east-1 bucket and the service accepted it, so the handler's client now sets followRegionRedirects: true, with a test on the client's config. A test with fromBucketAttributes({ region }) on the lib side wouldn't catch it, since the template is the same.

console.log(`Bucket '${target.Bucket}' does not exist; nothing to delete.`);
return;
}
if (error.name === 'AccessDenied') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When does the role lose access here? As far as I can tell, a destination change gives a new copy function that keeps its old grants, and CloudFormation deletes the copy before its function and policy in cleanup. Swallowing AccessDenied turns a grant regression or an SCP deny into UPDATE_COMPLETE with a leaked object version. Could this keep only NoSuchBucket?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right: the destination is part of the copy function's identity, so a moved destination gets a new function and the old one deletes with its old grants. The comment predates that. Removed in 726be1d; only NoSuchBucket is tolerated, and AccessDenied now fails the Delete (tested).

*/
private readonly crossStackPolicyArn?: { readonly webFunctionArn: string; readonly endpointName: string };

constructor(scope: Construct, id: string, props: WebFunctionEndpointProps) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The HOME_REGION-only rule for a function with an egressNetworkConnector is enforced in WebFunction and addEndpoint() (webfunction.ts:697) but not here. So new WebFunctionEndpoint(this, 'Ep', { webFunction: fnWithConnector, endpointLocation: WebFunctionEndpointLocation.multiRegion([...]), ... }) passes synth and the service rejects it at deploy. Running the same check here, plus a sentence in the README's network connector section, would cover it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f5018cd: the check now runs in WebFunctionEndpoint, so it covers the default endpoint, addEndpoint() and a standalone endpoint for a function defined in the app (an imported function's connector isn't known). The README's network connector section says so.

@piotrekwitkowski piotrekwitkowski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Three small doc items inline. The PR description has one more, and it becomes the squash commit message. It still says the copy "never prunes on update" and gets "list-versions on the destination key pattern". Since 5c3caf1f Delete removes the replaced version, and the copy role gets s3:GetObject and s3:GetObjectVersion on the source and put and delete on the destination (s3-object-copy.ts:171-177).

bucket. Such a change replaces the copy function, and the old copy is removed under its
previous policy.

One copy function is created per stack and shared by all WebFunctions in it. Pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The singleton key includes the destination (s3-object-copy.ts:230, -dest-..., plus role, VPC, security groups and log group), and the staged-asset integ snapshot has two copy functions, ...dest438c... and ...destb0df.... So it's one copy function per destination bucket and copy options rather than one per stack.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, fixed in b6833ae: one copy function for each destination bucket and set of copyOptions, shared by the WebFunctions in the stack that use them.

Comment on lines +2319 to +2320
define another policy for the same function: `AWS::Lambda::ResourcePolicy` replaces the whole
document. For the same reason the policy of an imported WebFunction cannot be added to; CDK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A second AWS::Lambda::ResourcePolicy for the same function doesn't replace the first. When I deployed one, CloudFormation rejected it with "already exists", at change-set validation from another stack and as CREATE_FAILED in the same stack. A policy set outside CloudFormation blocks it the same way, which I hit on an existing stack. So moving from an escape-hatch CfnResourcePolicy or an out-of-band policy to grants.invoke() takes two deploys, one to remove the old policy and one to add the grants.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for testing it. Fixed in b6833ae: the README now says a second AWS::Lambda::ResourcePolicy, or a policy set outside CloudFormation, fails with "already exists", and that moving to grants.invoke() takes two deploys, one to remove the old policy and one to add the grants.


## WebFunction

`WebFunction` is a serverless compute resource that lets you build and deploy low-latency web applications on AWS infrastructure. WebFunctions deploy as function endpoints spanning multiple regions, acting as global web servers — no API Gateway or CloudFront required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small one. The default endpoint is homeRegion(), so spanning multiple Regions is opt-in, and the Lambda guide sends users to CloudFront for a custom domain or AWS WAF, which the CloudFront section further down also covers. Something like "each WebFunction gets its own HTTPS endpoint, in one Region by default or across several" would match the rest of the README.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b6833ae: "Each WebFunction gets its own HTTPS endpoint, in one Region by default or across several."

The copy's key and removal policies are now read from the revision as
it is rendered, so escape-hatch overrides count (r4407233868). Its
configuration fingerprint keeps only references (Ref, Fn::GetAtt,
Fn::ImportValue) and hashes every other literal, replacing any intrinsic
it does not know by a hash with the references inside it kept apart and
conditional ones under their Fn::If; synthesis fails if a dynamic
reference is left (r4407233861). RetainExceptOnCreate is carried as such,
and a shared bucket takes the strongest policy of its revisions
(r4407233877).
The copy function follows S3's Region redirect, so a provided bucket in
another Region works; the service accepts code from one (r4407233885).
Delete no longer treats AccessDenied as success: a moved destination
gets a new copy function, so the old one keeps its grants, and a denial
is a real failure (r4407233890).
A function with an egress network connector can only be served by a
home-Region endpoint. The check now runs in WebFunctionEndpoint, so a
standalone endpoint is covered too (r4407233893).
New integ: a helper Lambda whose only permission is the statement
grants.invoke() adds to the function's resource policy signs a SigV4
request and gets a 200, and a MULTI_REGION and a PER_REGION endpoint
added with addEndpoint() are created and reject unsigned requests.
Also regenerates the staged-asset and network-connector snapshots,
whose copy configuration changed.
@licjun

licjun commented Oct 29, 2026

Copy link
Copy Markdown
Author

Re the coverage gap in the last review: added integ.webfunction-endpoints in 9c8aced and deployed it. A helper Lambda whose only permission is the statement endpoint.grants.invoke() adds to the function's resource policy (granted to an ArnPrincipal, so nothing lands on its role) signs a GET with SigV4 and X-Amz-Content-SHA256 and gets a 200. A MULTI_REGION and a PER_REGION endpoint added with addEndpoint() are created and return 403 to an unsigned request. The MULTI_REGION endpoint isn't called signed, since SigV4a needs CRT in the helper; your CloudFront test covered that path. cc @matboros

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review summary comment for the overview; the notes below are inline.

Comment on lines +390 to +398
* to serve the same function with a different auth type or endpoint type.
*/
@propertyInjectable
export class WebFunctionEndpoint extends WebFunctionEndpointBase {
/**
* Uniquely identifies this class.
*/
public static readonly PROPERTY_INJECTION_ID: string = 'aws-cdk-lib.aws-lambda.WebFunctionEndpoint';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Recommended — WebFunction and NetworkConnector each define a static type guard (isWebFunction / isNetworkConnector, backed by a Symbol.for), but the public WebFunctionEndpoint construct class defines neither an isWebFunctionEndpoint guard nor the backing symbol. The guidance says every construct class should expose a static isFoo(x): x is Foo and steer consumers away from instanceof (AGENTS_CONSTRUCT_DESIGN.md#static-type-check). Without it, a consumer holding an IWebFunctionEndpoint that needs to narrow to the concrete class is pushed toward instanceof, and the endpoint is inconsistent with its two sibling constructs added in this same change.

Suggested change: Add the same Symbol.for('@aws-cdk/aws-lambda.WebFunctionEndpoint') marker plus public static isWebFunctionEndpoint(x: any): x is WebFunctionEndpoint pattern used by WebFunction.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added in 51ce842: WebFunctionEndpoint.isWebFunctionEndpoint(), backed by Symbol.for('@aws-cdk/aws-lambda.WebFunctionEndpoint') like the other two. An imported endpoint is not one.

@@ -135,6 +135,7 @@
"props-physical-name:aws-cdk-lib.aws_lambda.EventSourceMappingProps",
"props-physical-name:aws-cdk-lib.aws_lambda.FunctionUrlProps",
"props-physical-name:aws-cdk-lib.aws_lambda.VersionProps",
"props-physical-name:aws-cdk-lib.aws_lambda.WebFunctionEndpointProps",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Recommended — This change adds props-physical-name:aws-cdk-lib.aws_lambda.WebFunctionEndpointProps to the awslint exclude list, suppressing the rule that enforces the {resource}Name physical-name convention. A suppression turns off a build-time guardrail for a design guideline, so it should carry a stated justification (CONTRIBUTING.md#awslint). The suppression itself looks defensible: the endpoint's physical-name prop is endpointName, matching the service term, which is the correct call under official-terminology naming rather than the awslint-expected webFunctionEndpointName — but nothing in the file or nearby records that reasoning for a future reader.

Suggested change: Record a short justification next to the entry (or in the PR description) noting that endpointName is the deliberate, service-aligned physical-name prop, so the dropped guardrail stays traceable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The justification is in the PR description, under "awslint exclusion": the endpoint's physical name is endpointName, the service's term (AWS::Lambda::WebFunctionEndpoint.EndpointName), rather than webFunctionEndpointName. awslint.json is plain JSON, so there is no place for a note next to the entry.

There is one copy function per destination bucket and copy options, not
per stack (r4407264531). A second resource policy, or one set outside
CloudFormation, fails with "already exists", so moving to the grants
takes two deploys (r4407264537). The default endpoint is in one Region
(r4407264541).
@licjun

licjun commented Oct 29, 2026

Copy link
Copy Markdown
Author

Updated the PR description, per the last review: an update leaves the old object version in place and it is deleted only when its copy is removed in cleanup (kept if its revision is retained), and the copy role gets s3:GetObject/s3:GetObjectVersion on the source objects and put/delete on the destination key pattern. I also corrected the code-bucket grant line, which said it was scoped to the code object: that holds for fromBucket and a provided bucket, while the managed bucket has one statement for web-function/* and any WebFunction revision in the account.

Like WebFunction and NetworkConnector, the endpoint gets a static type
guard backed by a Symbol.for marker (r4407790337).

This branch was successfully deployed

1 active deployment
automation — 51ce8428 Deployed Oct 29, 2026 by licjun via validate-pr #375473
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK p2 pr/needs-community-review This PR needs a review from a Trusted Community Member or Core Team Member. pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. pr/request-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants