Skip to content

fix: use alias for versioned lambda functions#6767

Open
anatolzak wants to merge 6 commits intoanomalyco:devfrom
anatolzak:worktree-fix-versioning-use-alias
Open

fix: use alias for versioned lambda functions#6767
anatolzak wants to merge 6 commits intoanomalyco:devfrom
anatolzak:worktree-fix-versioning-use-alias

Conversation

@anatolzak
Copy link
Copy Markdown
Contributor

@anatolzak anatolzak commented Apr 18, 2026

closes #6722 #6720

Summary

Reworks function versioning to always create a persistent LatestAlias when versioning (or durable) is enabled. targetArn, qualifier, Function URLs, URL permissions, and invoke ARNs now all route through this alias instead of a pinned numeric version.

Previously:

  • targetArn pointed at fn.qualifiedArn (a specific published version number that churns each deploy).
  • Durable functions created a one-off ${name}Durable alias inline, but only for the Function URL — other consumers still got the pinned version ARN.

Now:

  • A single ${name}LatestAlias is created whenever the function is published, and every downstream integration (URL, event sources, API GW, cron, workflow, SDK link, etc.) targets it.
  • fn.qualifier resolves to the alias name when versioning is on, and undefined when it isn't.
  • The URL is bound to the alias (no longer to $LATEST) as soon as versioning is enabled.

This is groundwork for upcoming rollout support — we need a stable indirection in front of versioned functions before we can implement traffic shifting.

What I tested

  • Workflow without a URL — correctly routes through the alias.
  • Workflow with a URL — URL is served off the alias.
  • Function with URL, no versioning — no regression, URL still targets the unqualified function.
  • Function with URL and versioning — alias created, URL targets the alias, permissions scoped to the alias.
  • Resource.<Fn>.qualifier via the SDK — resolves to the alias name when versioning is enabled, and has no value when it isn't.
  • Lambda behind API Gateway:
    • without versioning: integration and permissions target the function directly, as before.
    • with versioning: integration points at the alias and the lambda:InvokeFunction permission is scoped to the alias.
  • Response streaming path — targetResponseStreamingInvokeArn now rewrites against the alias ARN, and streamed invocations work.
  • AWS Router with protection (OAC) + Lambda no versioning — no regression, URL still targets the unqualified function.
  • AWS Router with protection (OAC) + Lambda with versioning — alias created, Router targets the alias, permissions scoped to the alias.
  • AWS SQS + Lambda no versioning — no regression, queue invoked the unqualified function.
  • AWS SQS + Lambda with versioning — alias created, SQS triggers the alias.

Upgrade notes

For any stack that previously enabled versioning: true or durable: true with a URL, the Function URL will be replaced:

  • versioning: true: the existing URL currently points at $LATEST. After upgrade it will point at the alias — a new URL will be issued.
  • durable: true: the alias changes from the old inline ${name}Durable to ${name}LatestAlias, which also causes a new URL to be issued.

Event sources that consume fn.targetArn (buckets, topics, crons, API Gateway integrations, etc.) will see a one-time update on upgrade as the ARN moves from a pinned version to the alias. This isn't new churn — previously, every deploy that changed function code published a new version, which forced those same event sources and permissions to update their target. With the alias, this happens once and then stops: subsequent deploys keep pointing at the same alias ARN, so downstream resources no longer need to be touched on every deploy.

Copy link
Copy Markdown
Collaborator

@vimtor vimtor left a comment

Choose a reason for hiding this comment

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

thanks for your contribution @anatolzak

i left some comments

Comment thread platform/src/components/aws/function.ts Outdated
tags: args.tags,
publish: output(args.versioning).apply(
(v) => v ?? Boolean(args.durable),
(v) => v || Boolean(args.durable),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why did we change this?

Comment thread platform/src/components/aws/function.ts Outdated
};
});
}
type NormalizedUrl = ReturnType<typeof normalizeUrl>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i don't think we need this

Comment thread platform/src/components/aws/function.ts Outdated

function normalizeUrl() {
return output(args.url).apply((url) => {
function normalizeUrl(url: FunctionArgs["url"]) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the normalize function usually don't take arguments

Comment thread platform/src/components/aws/function.ts Outdated
});
}

type CreateUrlOpts = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

inline types like this are very weird

* ID generation inside durable operations like `ctx.step()`.
*
* :::caution
* Workflow handlers have versioning enabled. Deploying an update won't update existing running workflows.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

@anatolzak anatolzak May 1, 2026

Choose a reason for hiding this comment

The 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 qualifiedArn, every consumer (URL, event sources, API GW, SDK qualifier, etc.) would have to be re-wired to roll back.

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 $LATEST escape hatch. No behavior regression.

Comment thread platform/src/components/aws/function.ts Outdated
return this.useQualifiedTarget.apply((useQualifiedTarget) =>
useQualifiedTarget ? this.function.qualifiedArn : this.arn,
);
return this.#targetArn;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why don't define targetArn here instead of adding this #targetArn?

const zipAsset = createZipAsset();
const fn = createFunction();
const urlEndpoint = createUrl();
const isVersioningEnabled = fn.publish.apply((publish) => Boolean(publish));
Copy link
Copy Markdown
Collaborator

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: true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@vimtor fn.publish is the resolved truth on the underlying aws.lambda.Function. Upstream at function.ts:2659, it's set to args.versioning || Boolean(args.durable), so checking fn.publish covers both paths in one expression

Comment thread platform/src/components/aws/function.ts Outdated
});
}

function createTargetArn(alias: Input<lambda.Alias | undefined>) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Comment thread platform/src/components/aws/function.ts Outdated

function createLatestUrl() {
return qualifier.apply((qualifier) =>
createUrl({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we should inline the createUrl function

or keep the createUrl, remove this createLatestUrl and move the logic to the existing method

@anatolzak
Copy link
Copy Markdown
Contributor Author

anatolzak commented May 1, 2026

@vimtor thanks for the review! I think I addressed all your comments.

A couple of notes on the changes you flagged:

The createLatestUrl wrapper around createUrl and the dynamic arguments on normalizeUrl were leftovers from the rollout PR, which needs to create two URLs (one per traffic-shifted version), so URL normalization and creation had to accept dynamic arguments. Since none of that applies to this PR, I reverted both back to their original signatures.

On using a private #targetArn field instead of a getter: I originally went that route to keep the diff minimal, but I switched to getters as you asked. That meant reordering the instance property assignments in the constructor, which I was trying to avoid to keep the diff minimal.

The chain is: createUrl now reads qualifier, qualifier reads targetArn, and targetArn reads this.function. When targetArn was a stored private field, createUrl could just read the captured value, so the order didn't matter. With a getter, every call walks back through this.function, so this.function has to be assigned before createUrl() runs. The new constructor order reflects that.

Let me know if there's anything else you'd like changed.

@anatolzak anatolzak force-pushed the worktree-fix-versioning-use-alias branch from 04350f1 to 1e105c1 Compare May 1, 2026 18:10
@anatolzak anatolzak requested a review from vimtor May 4, 2026 06:38
@anatolzak anatolzak changed the title fix: Route versioned functions through a stable alias fix: use alias for versioned lambda functions May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event sources should invoke Lambda through an alias when versioning is enabled

2 participants