Repository navigation
Conversation
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.
|
👋 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 |
|
|
||||||||||||||
|
|
||||||||||||||
|
Automated review A maintainer will still review this — treat the notes below as a starting point. 📌 Commit This PR adds net-new L2 constructs for AWS Lambda's WebFunction family — 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 🔴 0 blocking · 🟡 7 recommended · ⚪ 3 optional Files with findings (5)Generated automatically. React 👍 or 👎 to tell us whether this review helped, so we can improve these reviews. |
There was a problem hiding this comment.
See the review summary comment for the overview; the notes below are inline.
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.
There was a problem hiding this comment.
See the review summary comment for the overview; the notes below are inline.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 } { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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({}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Re the coverage gap in the last review: added |
There was a problem hiding this comment.
See the review summary comment for the overview; the notes below are inline.
| * 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'; | ||
|
|
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
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", | |||
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
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).
|
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 |
Like WebFunction and NetworkConnector, the endpoint gets a static type guard backed by a Symbol.for marker (r4407790337).
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::WebFunctionRevisionand
AWS::Lambda::WebFunctionEndpoint— which the caller has to wire togetherthemselves, including the dependency order between them and the
FunctionNameeach 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.fromAssetcannot beused and local code needs a staging step.
VPC egress works differently from
lambda.Function: a WebFunction does not takesubnets and security groups, it references an
AWS::Lambda::NetworkConnectorthatowns the network interfaces, so they are provisioned once with the connector rather
than per function.
Description of changes
WebFunctioncreates the function, one revision and one endpoint, and sets thedependencies and shared
FunctionNamebetween them. It exposesdomainName,endpointArn,revisionArnandrevisionId, implementsIGrantable, offersgrants.invoke(), and creates an execution role if none is given.authTypeisrequired with no default, so making an endpoint public is always explicit.
WebFunctionCodehas three factories:fromBucket— code already in a compliant bucket; the bucket is validated, notmodified.
fromAssetToManagedBucket— a local asset staged into a per-stack bucket CDKcreates with versioning, SSE-S3 and the service-principal read policy.
fromAssetToProvidedBucket— a local asset staged into the caller's bucket, whichis validated for versioning and encryption.
Staging uses a new internal custom resource that copies one object server-side with
CopyObjectand returns theVersionIdit wrote, which the revision pins. This iswhy it is not
BucketDeployment: that resource does not report the written version,so getting one would need a second custom resource calling
headObject, whichreturns 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
/tmpceiling.NetworkConnectorwrapsAWS::Lambda::NetworkConnector. Its configuration ischosen through
NetworkConnectorConfiguration.vpcEgress({...}), which takes avpcplus an optional
vpcSubnetsselection and the security groups. By default theconstruct creates the operator role the service assumes to manage the interfaces and
attaches the
AWSLambdaNetworkConnectorOperatorPolicymanaged policy; a role definedin 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.egressNetworkConnectorpins the revision to the connector'slatestVersionArn. A connector in the same stack is therefore replaced-and-movedas 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.fromNetworkConnectorVersionArnis the way to pin a fixed version, including across stacks.
Describe any new or updated permissions being added
s3:GetObjectands3:GetObjectVersiontolambda.amazonaws.com, conditioned onaws:SourceAccountandaws:SourceArn. ForfromBucketand a provided bucket, it is scoped to the function's code and itsrevisions; for the managed bucket, one statement covers
web-function/*and anyWebFunction revision in the account.
conditions, so the service can decrypt the code and environment variables.
s3:GetObjectands3:GetObjectVersionon thesource bucket's objects, and put and delete on the destination key pattern.
AWSLambdaBasicExecutionRole.grants.invoke()grantslambda:InvokeWebFunctionEndpointon the endpoint ARN.NetworkConnector's default operator role is created withAWSLambdaNetworkConnectorOperatorPolicyand trustslambda.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 alocal 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_AUTHan unsigned request gets403, whichconfirms the endpoint is live and enforcing auth. The connector test uses an
APPLICATION_MANAGEDendpoint and expects200with an object the handler can onlyread 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.WebFunctionEndpointPropsis excluded onpurpose: the endpoint's physical name is
endpointName, the service's term(
AWS::Lambda::WebFunctionEndpoint.EndpointName), rather thanwebFunctionEndpointName.Not in this PR: weighted traffic
WebFunctionTraffichas onlysingle(). AWebFunctionkeeps one revision, so there is nosecond revision to split traffic with; weights need addressable, retained revisions, which
can be added later together with a
WebFunctionTraffic.weighted()factory without changingsingle()or thetrafficprop. Until then, useendpoint.resourceto setRevisionWeights.Integ snapshot changes
integ.webfunction-network-connectorandinteg.webfunction-staged-assetgive theirCustom::S3ObjectCopyresources new logical ids: a copy's id now depends only on the copyfunction, 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