diff --git a/platform/src/components/aws/function.ts b/platform/src/components/aws/function.ts index ab8aa24f2b..7f85594407 100644 --- a/platform/src/components/aws/function.ts +++ b/platform/src/components/aws/function.ts @@ -1309,6 +1309,12 @@ export interface FunctionArgs { /** * Enable versioning for the function. * + * :::caution + * If you're wiring this function to an event source by passing its ARN, + * use `fn.targetArn` rather than `fn.arn`. Using `fn.arn` invokes + * `$LATEST` and bypasses the alias that routes to the published version. + * ::: + * * :::note * Durable functions enable this by default. * ::: @@ -1476,6 +1482,12 @@ export interface FunctionArgs { * This property is meant to be used internally by [Workflow](/docs/component/aws/workflow). * Prefer the component if you want to use the [SDK](/docs/component/aws/workflow#sdk) or if you are not very familiar with durable functions limitations. * ::: + * + * :::caution + * If you're wiring this function to an event source by passing its ARN, + * use `fn.targetArn` rather than `fn.arn`. Using `fn.arn` invokes + * `$LATEST` and bypasses the alias that routes to the published version. + * ::: */ durable?: | boolean @@ -1500,6 +1512,11 @@ export interface FunctionArgs { * Transform the Lambda Function resource. */ function?: Transform; + /** + * Transform the Lambda Alias resource. This is only created when versioning + * is enabled (via `versioning: true` or `durable: true`). + */ + alias?: Transform; /** * Transform the IAM Role resource. */ @@ -1712,6 +1729,8 @@ export class Function extends Component implements Link.Linkable { private logGroup: Output; private urlEndpoint: Output; private eventInvokeConfig?: lambda.FunctionEventInvokeConfig; + private alias: Output; + private isVersioningEnabled: Output; private static readonly encryptionKey = lazy( () => @@ -1781,7 +1800,8 @@ export class Function extends Component implements Link.Linkable { const logGroup = createLogGroup(); const zipAsset = createZipAsset(); const fn = createFunction(); - const urlEndpoint = createUrl(); + const isVersioningEnabled = fn.publish.apply((publish) => Boolean(publish)); + const alias = createLatestAlias(); createProvisioned(); const eventInvokeConfig = createEventInvokeConfig(); @@ -1790,8 +1810,14 @@ export class Function extends Component implements Link.Linkable { this.function = fn; this.role = role; this.logGroup = logGroup; - this.urlEndpoint = urlEndpoint; this.eventInvokeConfig = eventInvokeConfig; + this.alias = alias; + this.isVersioningEnabled = isVersioningEnabled; + + const urlEndpoint = this.qualifier.apply((qualifier) => + createUrl(qualifier), + ); + this.urlEndpoint = urlEndpoint; const buildInput = output({ functionID: name, @@ -2700,7 +2726,25 @@ export class Function extends Component implements Link.Linkable { ); } - function createUrl() { + function createLatestAlias() { + return isVersioningEnabled.apply((isVersioningEnabled) => { + if (!isVersioningEnabled) return; + + return new lambda.Alias( + ...transform( + args.transform?.alias, + `${name}LatestAlias`, + { + functionName: fn.name, + functionVersion: fn.version, + }, + { parent }, + ), + ); + }); + } + + function createUrl(qualifier: string | undefined) { return url.apply((url) => { if (url === undefined) return output(undefined); @@ -2712,23 +2756,10 @@ export class Function extends Component implements Link.Linkable { ([oac, authorization]) => oac || authorization === "iam", ); - /** - * Lambda Function URLs only accept alias names in the explicit `qualifier` - * field. Durable functions with URLs therefore need an alias target here, - * even when the underlying function is still on `$LATEST`. - * See https://github.com/hashicorp/terraform-provider-aws/issues/31459 - */ - const qualifier = durable - ? new lambda.Alias(`${name}Durable`, { - functionName: fn.arn, - functionVersion: fn.version, - }).name - : undefined; - const fnUrl = new lambda.FunctionUrl( `${name}Url`, { - functionName: durable ? fn.arn : fn.name, + functionName: fn.name, qualifier, authorizationType: isIam.apply((isIam) => isIam ? "AWS_IAM" : "NONE", @@ -2738,7 +2769,7 @@ export class Function extends Component implements Link.Linkable { ), cors: url.cors, }, - { parent }, + { parent, deleteBeforeReplace: false }, ); if (!url.route) { @@ -2750,6 +2781,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunctionUrl", function: fn.name, + qualifier, principal: "*", functionUrlAuthType: "NONE", }, @@ -2760,6 +2792,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunction", function: fn.name, + qualifier, principal: "*", invokedViaFunctionUrl: true, }, @@ -2778,6 +2811,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunctionUrl", function: fn.name, + qualifier, principal: "cloudfront.amazonaws.com", sourceArn: distributionArn, }, @@ -2788,6 +2822,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunction", function: fn.name, + qualifier, principal: "cloudfront.amazonaws.com", sourceArn: distributionArn, invokedViaFunctionUrl: true, @@ -2800,6 +2835,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunctionUrl", function: fn.name, + qualifier, principal: "*", functionUrlAuthType: "NONE", }, @@ -2810,6 +2846,7 @@ export class Function extends Component implements Link.Linkable { { action: "lambda:InvokeFunction", function: fn.name, + qualifier, principal: "*", invokedViaFunctionUrl: true, }, @@ -2952,6 +2989,10 @@ export class Function extends Component implements Link.Linkable { * The Function Event Invoke Config resource if retries are configured. */ eventInvokeConfig: this.eventInvokeConfig, + /** + * The Lambda Alias. Available when `versioning` or `durable` is enabled. + */ + alias: this.alias, }; } @@ -2983,18 +3024,21 @@ export class Function extends Component implements Link.Linkable { return this.function.arn; } - /** @internal */ - private get useQualifiedTarget() { - return this.function.publish.apply( - (publish) => (publish ?? false) || this.durable, - ); - } - - /** @internal */ + /** + * The ARN to use when wiring this function to an event source. + * + * When versioning is enabled, this points to the Lambda alias that routes + * to the currently published version. Otherwise it falls back to the + * function ARN. + * + * Prefer this over `arn` when passing the function to an event source, so + * invocations go through the alias instead of `$LATEST`. + */ public get targetArn() { - return this.useQualifiedTarget.apply((useQualifiedTarget) => - useQualifiedTarget ? this.function.qualifiedArn : this.arn, - ); + return this.alias.apply((alias) => { + if (alias) return alias.arn; + return this.function.arn; + }); } /** @internal */ @@ -3006,23 +3050,21 @@ export class Function extends Component implements Link.Linkable { /** @internal */ public get targetInvokeArn() { - return this.useQualifiedTarget.apply((useQualifiedTarget) => - useQualifiedTarget - ? this.function.qualifiedInvokeArn - : this.function.invokeArn, + return this.alias.apply((alias) => + alias ? alias.invokeArn : this.function.invokeArn, ); } /** @internal */ public get targetResponseStreamingInvokeArn() { - return this.useQualifiedTarget.apply((useQualifiedTarget) => - useQualifiedTarget + return this.alias.apply((alias) => + alias ? all([ this.arn, - this.function.qualifiedArn, + alias.arn, this.function.responseStreamingInvokeArn, - ]).apply(([arn, qualifiedArn, responseStreamingInvokeArn]) => - responseStreamingInvokeArn.replace(arn, qualifiedArn), + ]).apply(([arn, aliasArn, responseStreamingInvokeArn]) => + responseStreamingInvokeArn.replace(arn, aliasArn), ) : this.function.responseStreamingInvokeArn, ); @@ -3110,11 +3152,7 @@ export class Function extends Component implements Link.Linkable { properties: { name: this.name, url: this.urlEndpoint, - ...(this.durable - ? { - qualifier: this.qualifier, - } - : {}), + qualifier: this.qualifier, }, include: [ permission({ @@ -3132,7 +3170,13 @@ export class Function extends Component implements Link.Linkable { ] : []), ], - resources: [this.durable ? interpolate`${this.arn}:*` : this.arn], + resources: this.isVersioningEnabled.apply((isVersioningEnabled) => + this.durable + ? [interpolate`${this.arn}:*`] + : isVersioningEnabled + ? [this.arn, interpolate`${this.arn}:*`] + : [this.arn], + ), }), ], }; diff --git a/platform/src/components/aws/workflow.ts b/platform/src/components/aws/workflow.ts index ba4657dca9..63a25a65fb 100644 --- a/platform/src/components/aws/workflow.ts +++ b/platform/src/components/aws/workflow.ts @@ -272,7 +272,12 @@ export interface WorkflowArgs * ID generation inside durable operations like `ctx.step()`. * * :::caution - * Workflow handlers have versioning enabled. Deploying an update won't update existing running workflows. + * The workflow SDK invokes the handler through an alias. Each execution is pinned + * to the version the alias points to when it starts, and stays pinned across resumes + * and retries — so deploying an update won't affect already-running workflows. + * + * To resume on the latest code, invoke the function directly via the Lambda SDK + * with the `$LATEST` qualifier. Not recommended for production. * ::: * * Before using workflows in production, review the diff --git a/platform/src/components/component.ts b/platform/src/components/component.ts index 919f023003..fb32177e86 100644 --- a/platform/src/components/component.ts +++ b/platform/src/components/component.ts @@ -147,6 +147,7 @@ export class Component extends ComponentResource { "aws:cognito/identityPoolRoleAttachment:IdentityPoolRoleAttachment", "aws:cognito/identityProvider:IdentityProvider", "aws:cognito/userPoolClient:UserPoolClient", + "aws:lambda/alias:Alias", "aws:lambda/eventSourceMapping:EventSourceMapping", "aws:lambda/functionEventInvokeConfig:FunctionEventInvokeConfig", "aws:lambda/functionUrl:FunctionUrl",