From ce8177c70b8e41ba45ac2c6018cb7f64ef104ef5 Mon Sep 17 00:00:00 2001 From: AppleDannyClegg <117488935+AppleDannyClegg@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:49:36 +0100 Subject: [PATCH] Fix intermittent Lambda 409 when adding Function URL permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploying an SSR site (Nextjs/Astro/etc.) intermittently fails with: operation error Lambda: AddPermission, StatusCode: 409, ResourceConflictException: The function could not be updated due to a concurrent update operation. Lambda serialises mutating control-plane operations per function. During a deploy, two resources mutate the same function but have no ordering between them, so Pulumi runs them concurrently: - the `aws.lambda.FunctionUrl` created in `Function.createUrl()` (CreateFunctionUrlConfig), and - the `aws.lambda.Permission` granting `lambda:InvokeFunctionUrl`. For the OAC path the permission is created in `SsrSite` and depends only on the function, not on the function URL — and the URL resource is not reachable because it is not exposed on `Function.nodes`. The permission's AddPermission call therefore lands while CreateFunctionUrlConfig still has the function in `LastUpdateStatus: InProgress`, yielding the 409. Fresh stacks hit it far more often because the function and its URL are created together in one run. Serialise the permission after the function URL: - expose the `FunctionUrl` resource on `Function.nodes.url`, and - add `dependsOn` on it to every `lambda.Permission` that grants URL access, both the ones created inside `Function.createUrl()` and the OAC/CloudFront ones created in `SsrSite`. `dependsOn` is metadata-only, so existing permissions are not replaced. --- platform/src/components/aws/function.ts | 38 +++++++++++++++++-------- platform/src/components/aws/ssr-site.ts | 16 +++++++---- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/platform/src/components/aws/function.ts b/platform/src/components/aws/function.ts index 1da49a2666..1742c953d1 100644 --- a/platform/src/components/aws/function.ts +++ b/platform/src/components/aws/function.ts @@ -1717,6 +1717,7 @@ export class Function extends Component implements Link.Linkable { private role: iam.Role; private logGroup: Output; private urlEndpoint: Output; + private urlResource: Output; private eventInvokeConfig?: lambda.FunctionEventInvokeConfig; private static readonly encryptionKey = lazy( @@ -1787,7 +1788,7 @@ export class Function extends Component implements Link.Linkable { const logGroup = createLogGroup(); const zipAsset = createZipAsset(); const fn = createFunction(); - const urlEndpoint = createUrl(); + const { endpoint: urlEndpoint, resource: urlResource } = createUrl(); createProvisioned(); const eventInvokeConfig = createEventInvokeConfig(); @@ -1797,6 +1798,7 @@ export class Function extends Component implements Link.Linkable { this.role = role; this.logGroup = logGroup; this.urlEndpoint = urlEndpoint; + this.urlResource = urlResource; this.eventInvokeConfig = eventInvokeConfig; const buildInput = output({ @@ -2708,9 +2710,13 @@ export class Function extends Component implements Link.Linkable { ); } - function createUrl() { - return url.apply((url) => { - if (url === undefined) return output(undefined); + function createUrl(): { + endpoint: Output; + resource: Output; + } { + const result = url.apply((url) => { + if (url === undefined) + return { endpoint: output(undefined), resource: undefined }; const authorization = output(url.authorization ?? "none"); const isOac = output(url.route?.routerProtection).apply( @@ -2761,7 +2767,7 @@ export class Function extends Component implements Link.Linkable { principal: "*", functionUrlAuthType: "NONE", }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); new lambda.Permission( `${name}InvokeFunction`, @@ -2771,10 +2777,10 @@ export class Function extends Component implements Link.Linkable { principal: "*", invokedViaFunctionUrl: true, }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); }); - return fnUrl.functionUrl; + return { endpoint: fnUrl.functionUrl, resource: fnUrl }; } // Create permissions based on Router protection mode @@ -2789,7 +2795,7 @@ export class Function extends Component implements Link.Linkable { principal: "cloudfront.amazonaws.com", sourceArn: distributionArn, }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); new lambda.Permission( `${name}CloudFrontInvokeFunction`, @@ -2800,7 +2806,7 @@ export class Function extends Component implements Link.Linkable { sourceArn: distributionArn, invokedViaFunctionUrl: true, }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); } else if (authorization === "none") { new lambda.Permission( @@ -2811,7 +2817,7 @@ export class Function extends Component implements Link.Linkable { principal: "*", functionUrlAuthType: "NONE", }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); new lambda.Permission( `${name}PublicInvokeFunction`, @@ -2821,7 +2827,7 @@ export class Function extends Component implements Link.Linkable { principal: "*", invokedViaFunctionUrl: true, }, - { parent }, + { parent, dependsOn: [fnUrl] }, ); } }, @@ -2890,8 +2896,12 @@ export class Function extends Component implements Link.Linkable { }, { parent }, ); - return url.route.routerUrl; + return { endpoint: url.route.routerUrl, resource: fnUrl }; }); + return { + endpoint: result.apply((r) => r.endpoint), + resource: result.apply((r) => r.resource), + }; } function createProvisioned() { @@ -2960,6 +2970,10 @@ export class Function extends Component implements Link.Linkable { * The Function Event Invoke Config resource if retries are configured. */ eventInvokeConfig: this.eventInvokeConfig, + /** + * The Lambda Function URL resource if `url` is enabled. + */ + url: this.urlResource, }; } diff --git a/platform/src/components/aws/ssr-site.ts b/platform/src/components/aws/ssr-site.ts index f8063d18f3..3febd595e1 100644 --- a/platform/src/components/aws/ssr-site.ts +++ b/platform/src/components/aws/ssr-site.ts @@ -1074,6 +1074,7 @@ async function handler(event) { // Server functions servers.forEach(({ region, server }) => { const provider = useProvider(region); + const urlDependsOn = server.nodes.url.apply((u) => (u ? [u] : [])); if (protection.mode === "none") { new lambda.Permission( @@ -1084,7 +1085,7 @@ async function handler(event) { principal: "*", functionUrlAuthType: "NONE", }, - { provider, parent: self }, + { provider, parent: self, dependsOn: urlDependsOn }, ); } else if ( protection.mode === "oac" || @@ -1098,7 +1099,7 @@ async function handler(event) { principal: "cloudfront.amazonaws.com", sourceArn: distributionArn, }, - { provider, parent: self }, + { provider, parent: self, dependsOn: urlDependsOn }, ); new lambda.Permission( `${name}CloudFrontInvokeFunction${logicalName(region)}`, @@ -1109,13 +1110,16 @@ async function handler(event) { sourceArn: distributionArn, invokedViaFunctionUrl: true, }, - { provider, parent: self }, + { provider, parent: self, dependsOn: urlDependsOn }, ); } }); // Image optimizer if (imgOptimizer) { + const urlDependsOn = imgOptimizer.nodes.url.apply((u) => + u ? [u] : [], + ); if (protection.mode === "none") { new lambda.Permission( `${name}ImageOptimizerPublicFunctionUrlAccess`, @@ -1125,7 +1129,7 @@ async function handler(event) { principal: "*", functionUrlAuthType: "NONE", }, - { parent: self }, + { parent: self, dependsOn: urlDependsOn }, ); } else if ( protection.mode === "oac" || @@ -1139,7 +1143,7 @@ async function handler(event) { principal: "cloudfront.amazonaws.com", sourceArn: distributionArn, }, - { parent: self }, + { parent: self, dependsOn: urlDependsOn }, ); new lambda.Permission( `${name}ImageOptimizerCloudFrontInvokeFunction`, @@ -1150,7 +1154,7 @@ async function handler(event) { sourceArn: distributionArn, invokedViaFunctionUrl: true, }, - { parent: self }, + { parent: self, dependsOn: urlDependsOn }, ); } }