Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 107 additions & 50 deletions platform/src/components/aws/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* :::
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
() =>
Expand Down Expand Up @@ -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 ?? []);
Expand All @@ -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));
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

const alias = createLatestAlias();
const targetArn = createTargetArn(alias);
const qualifier = output(targetArn).apply(
(arn) => splitQualifiedFunctionArn(arn).qualifier,
);
const urlEndpoint = createLatestUrl();
createProvisioned();
const eventInvokeConfig = createEventInvokeConfig();

Expand All @@ -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,
Expand Down Expand Up @@ -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"]) {
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

return output(url).apply((url) => {
if (url === false || url === undefined) return;
if (url === true) {
url = {};
Expand Down Expand Up @@ -2002,6 +2028,7 @@ export class Function extends Component implements Link.Linkable {
};
});
}
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


function normalizeCopyFiles() {
return output(args.copyFiles ?? []).apply((copyFiles) =>
Expand Down Expand Up @@ -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),
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?

),
reservedConcurrentExecutions: concurrency?.reserved,
durableConfig: durable && {
Expand Down Expand Up @@ -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>) {
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

return output(alias).apply((alias) => {
if (alias) return alias.arn;
return fn.arn;
});
}

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

qualifier: string | undefined;
url: NormalizedUrl;
};
function createUrl({ url, qualifier }: CreateUrlOpts) {
return url.apply((url) => {
if (url === undefined) return output(undefined);

Expand All @@ -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",
Expand All @@ -2738,7 +2778,7 @@ export class Function extends Component implements Link.Linkable {
),
cors: url.cors,
},
{ parent },
{ parent, deleteBeforeReplace: false },
);

if (!url.route) {
Expand All @@ -2750,6 +2790,7 @@ export class Function extends Component implements Link.Linkable {
{
action: "lambda:InvokeFunctionUrl",
function: fn.name,
qualifier,
principal: "*",
functionUrlAuthType: "NONE",
},
Expand All @@ -2760,6 +2801,7 @@ export class Function extends Component implements Link.Linkable {
{
action: "lambda:InvokeFunction",
function: fn.name,
qualifier,
principal: "*",
invokedViaFunctionUrl: true,
},
Expand All @@ -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,
},
Expand All @@ -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,
Expand All @@ -2800,6 +2844,7 @@ export class Function extends Component implements Link.Linkable {
{
action: "lambda:InvokeFunctionUrl",
function: fn.name,
qualifier,
principal: "*",
functionUrlAuthType: "NONE",
},
Expand All @@ -2810,6 +2855,7 @@ export class Function extends Component implements Link.Linkable {
{
action: "lambda:InvokeFunction",
function: fn.name,
qualifier,
principal: "*",
invokedViaFunctionUrl: true,
},
Expand Down Expand Up @@ -2886,6 +2932,15 @@ export class Function extends Component implements Link.Linkable {
});
}

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

url,
qualifier,
}),
);
}

function createProvisioned() {
return all([args.concurrency, fn.publish]).apply(
([concurrency, publish]) => {
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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;
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?

}

/** @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,
);
Expand Down Expand Up @@ -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({
Expand All @@ -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],
),
}),
],
};
Expand Down
7 changes: 6 additions & 1 deletion platform/src/components/aws/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.

* 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
Expand Down
1 change: 1 addition & 0 deletions platform/src/components/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading