-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: use alias for versioned lambda functions #6767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 2 commits
0e3e96d
207829d
0a7d668
5f2231e
1e105c1
6cf7667
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -1712,6 +1724,10 @@ export class Function extends Component implements Link.Linkable { | |
| private logGroup: Output<cloudwatch.LogGroup | undefined>; | ||
| private urlEndpoint: Output<string | undefined>; | ||
| private eventInvokeConfig?: lambda.FunctionEventInvokeConfig; | ||
| private alias: Output<lambda.Alias | undefined>; | ||
| private isVersioningEnabled: Output<boolean>; | ||
| #targetArn: Output<string>; | ||
| #qualifier: Output<string | undefined>; | ||
|
|
||
| private static readonly encryptionKey = lazy( | ||
| () => | ||
|
|
@@ -1765,7 +1781,7 @@ export class Function extends Component implements Link.Linkable { | |
| const streaming = normalizeStreaming(); | ||
| const logging = normalizeLogging(); | ||
| const volume = normalizeVolume(); | ||
| const url = normalizeUrl(); | ||
| const url = normalizeUrl(args.url); | ||
| const copyFiles = normalizeCopyFiles(); | ||
| const durable = normalizeDurable(); | ||
| const policies = output(args.policies ?? []); | ||
|
|
@@ -1781,7 +1797,13 @@ 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(); | ||
| const targetArn = createTargetArn(alias); | ||
| const qualifier = output(targetArn).apply( | ||
| (arn) => splitQualifiedFunctionArn(arn).qualifier, | ||
| ); | ||
| const urlEndpoint = createLatestUrl(); | ||
| createProvisioned(); | ||
| const eventInvokeConfig = createEventInvokeConfig(); | ||
|
|
||
|
|
@@ -1792,6 +1814,10 @@ export class Function extends Component implements Link.Linkable { | |
| this.logGroup = logGroup; | ||
| this.urlEndpoint = urlEndpoint; | ||
| this.eventInvokeConfig = eventInvokeConfig; | ||
| this.alias = alias; | ||
| this.#targetArn = targetArn; | ||
| this.#qualifier = qualifier; | ||
| this.isVersioningEnabled = isVersioningEnabled; | ||
|
|
||
| const buildInput = output({ | ||
| functionID: name, | ||
|
|
@@ -1967,8 +1993,8 @@ export class Function extends Component implements Link.Linkable { | |
| })); | ||
| } | ||
|
|
||
| function normalizeUrl() { | ||
| return output(args.url).apply((url) => { | ||
| function normalizeUrl(url: FunctionArgs["url"]) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the normalize function usually don't take arguments |
||
| return output(url).apply((url) => { | ||
| if (url === false || url === undefined) return; | ||
| if (url === true) { | ||
| url = {}; | ||
|
|
@@ -2002,6 +2028,7 @@ export class Function extends Component implements Link.Linkable { | |
| }; | ||
| }); | ||
| } | ||
| type NormalizedUrl = ReturnType<typeof normalizeUrl>; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't think we need this |
||
|
|
||
| function normalizeCopyFiles() { | ||
| return output(args.copyFiles ?? []).apply((copyFiles) => | ||
|
|
@@ -2630,7 +2657,7 @@ export class Function extends Component implements Link.Linkable { | |
| layers: args.layers, | ||
| tags: args.tags, | ||
| publish: output(args.versioning).apply( | ||
| (v) => v ?? Boolean(args.durable), | ||
| (v) => v || Boolean(args.durable), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why did we change this? |
||
| ), | ||
| reservedConcurrentExecutions: concurrency?.reserved, | ||
| durableConfig: durable && { | ||
|
|
@@ -2700,7 +2727,33 @@ export class Function extends Component implements Link.Linkable { | |
| ); | ||
| } | ||
|
|
||
| function createUrl() { | ||
| function createLatestAlias() { | ||
| return isVersioningEnabled.apply((isVersioningEnabled) => { | ||
| if (!isVersioningEnabled) return; | ||
|
|
||
| return new lambda.Alias( | ||
| `${name}LatestAlias`, | ||
| { | ||
| functionName: fn.name, | ||
| functionVersion: fn.version, | ||
| }, | ||
| { parent }, | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| function createTargetArn(alias: Input<lambda.Alias | undefined>) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is weird because it's not creating anything. that's why i said to use the getter directly in a comment below |
||
| return output(alias).apply((alias) => { | ||
| if (alias) return alias.arn; | ||
| return fn.arn; | ||
| }); | ||
| } | ||
|
|
||
| type CreateUrlOpts = { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inline types like this are very weird |
||
| qualifier: string | undefined; | ||
| url: NormalizedUrl; | ||
| }; | ||
| function createUrl({ url, qualifier }: CreateUrlOpts) { | ||
| return url.apply((url) => { | ||
| if (url === undefined) return output(undefined); | ||
|
|
||
|
|
@@ -2712,23 +2765,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 +2778,7 @@ export class Function extends Component implements Link.Linkable { | |
| ), | ||
| cors: url.cors, | ||
| }, | ||
| { parent }, | ||
| { parent, deleteBeforeReplace: false }, | ||
| ); | ||
|
|
||
| if (!url.route) { | ||
|
|
@@ -2750,6 +2790,7 @@ export class Function extends Component implements Link.Linkable { | |
| { | ||
| action: "lambda:InvokeFunctionUrl", | ||
| function: fn.name, | ||
| qualifier, | ||
| principal: "*", | ||
| functionUrlAuthType: "NONE", | ||
| }, | ||
|
|
@@ -2760,6 +2801,7 @@ export class Function extends Component implements Link.Linkable { | |
| { | ||
| action: "lambda:InvokeFunction", | ||
| function: fn.name, | ||
| qualifier, | ||
| principal: "*", | ||
| invokedViaFunctionUrl: true, | ||
| }, | ||
|
|
@@ -2778,6 +2820,7 @@ export class Function extends Component implements Link.Linkable { | |
| { | ||
| action: "lambda:InvokeFunctionUrl", | ||
| function: fn.name, | ||
| qualifier, | ||
| principal: "cloudfront.amazonaws.com", | ||
| sourceArn: distributionArn, | ||
| }, | ||
|
|
@@ -2788,6 +2831,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 +2844,7 @@ export class Function extends Component implements Link.Linkable { | |
| { | ||
| action: "lambda:InvokeFunctionUrl", | ||
| function: fn.name, | ||
| qualifier, | ||
| principal: "*", | ||
| functionUrlAuthType: "NONE", | ||
| }, | ||
|
|
@@ -2810,6 +2855,7 @@ export class Function extends Component implements Link.Linkable { | |
| { | ||
| action: "lambda:InvokeFunction", | ||
| function: fn.name, | ||
| qualifier, | ||
| principal: "*", | ||
| invokedViaFunctionUrl: true, | ||
| }, | ||
|
|
@@ -2886,6 +2932,15 @@ export class Function extends Component implements Link.Linkable { | |
| }); | ||
| } | ||
|
|
||
| function createLatestUrl() { | ||
| return qualifier.apply((qualifier) => | ||
| createUrl({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should inline the or keep the |
||
| url, | ||
| qualifier, | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| function createProvisioned() { | ||
| return all([args.concurrency, fn.publish]).apply( | ||
| ([concurrency, publish]) => { | ||
|
|
@@ -2952,6 +3007,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,46 +3042,42 @@ 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.#targetArn; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why don't define targetArn here instead of adding this |
||
| } | ||
|
|
||
| /** @internal */ | ||
| public get qualifier() { | ||
| return this.targetArn.apply( | ||
| (arn) => splitQualifiedFunctionArn(arn).qualifier, | ||
| ); | ||
| return this.#qualifier; | ||
| } | ||
|
|
||
| /** @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 +3165,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 +3183,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], | ||
| ), | ||
| }), | ||
| ], | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should keep the versioned behaviour. i don't understand why we changed this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vimtor The versioned behavior is unchanged. The main reason durable workflows version the handler is so that an in-flight execution resumes on the same code it started on. That's preserved: the lanbda API captures the resolved version at execution start and pins all resumes/retries to that captured version, regardless of whether the alias is later moved. What the alias buys us, beyond what a pinned numeric version gives, is decoupling the handler identity from the deployed version. If you ship a bad release and need to roll back, you just point the alias at the previous version, and no consumers have to be updated. Any newly-started executions immediately run against the rolled-back version. With a pinned It also opens the door to weighted aliases: you can split traffic across two versions of a workflow handler for A/B tests or canary rollouts without touching any client. None of that is possible if consumers point at a specific numeric version. The doc rewrite just makes the mechanism explicit and adds the |
||
| * 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can also define versioning with
versioning: trueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vimtor
fn.publishis the resolved truth on the underlyingaws.lambda.Function. Upstream at function.ts:2659, it's set toargs.versioning || Boolean(args.durable), so checkingfn.publishcovers both paths in one expression