1import * as Lambda from "@distilled.cloud/aws/lambda";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { deepEqual, isResolved } from "../../Diff.ts";
6import type { Input } from "../../Input.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 syncEventInvokeConfig,
14 type EventInvokeConfig,
15} from "./EventInvokeConfig.ts";
16import type { Version } from "./Version.ts";
17
18export interface AliasProps {
19 /**
20 * Managed Lambda version that this alias invokes.
21 */
22 version: Version;
23 /**
24 * Name of the alias. If omitted, a unique name is generated.
25 */
26 aliasName?: string;
27 /**
28 * Description of the alias.
29 */
30 description?: string;
31 /**
32 * Weighted routing configuration for shifting traffic to an additional
33 * function version.
34 */
35 routingConfig?: Lambda.AliasRoutingConfiguration;
36 /**
37 * Asynchronous invocation settings (retries, event age, destinations)
38 * scoped to this alias. Omit to remove any existing alias-level config and
39 * fall back to Lambda's defaults (2 retries, 6-hour max event age, no
40 * destinations).
41 */
42 eventInvokeConfig?: EventInvokeConfig;
43}
44
45export interface Alias extends Resource<
46 "AWS.Lambda.Alias",
47 AliasProps,
48 {
49 /**
50 * ARN of the Lambda alias.
51 */
52 aliasArn: string;
53 /**
54 * Name of the alias.
55 */
56 aliasName: string;
57 /**
58 * Name or ARN of the Lambda function this alias belongs to.
59 */
60 functionName: string;
61 /**
62 * Lambda function version that this alias invokes.
63 */
64 functionVersion: string;
65 /**
66 * API Gateway-compatible invocation ARN for this alias.
67 */
68 invokeArn: string;
69 /**
70 * Description of the alias.
71 */
72 description?: string;
73 /**
74 * Weighted routing configuration for this alias.
75 */
76 routingConfig?: Lambda.AliasRoutingConfiguration;
77 /**
78 * Latest Lambda revision id for this alias.
79 */
80 revisionId?: string;
81 },
82 never,
83 Providers
84> {}
85
86type ResolvedAliasProps = Input.ResolveProps<AliasProps>;
87
88const resolvedProps = (props: AliasProps): ResolvedAliasProps =>
89 props as unknown as ResolvedAliasProps;
90
91/**
92 * A Lambda alias for routing invocations to a stable function version.
93 *
94 * ### Creating Aliases
95 * **Example:** Production Alias
96 * ```typescript
97 * const version = yield* Version("ProductionVersion", { function: fn });
98 * const alias = yield* Alias("ProductionAlias", {
99 * version,
100 * aliasName: "production",
101 * });
102 * ```
103 *
104 * ### Weighted Routing
105 * **Example:** Shift Traffic to Another Version
106 * ```typescript
107 * const version = yield* Version("LiveVersion", { function: fn });
108 * const alias = yield* Alias("LiveAlias", {
109 * version,
110 * aliasName: "live",
111 * routingConfig: {
112 * AdditionalVersionWeights: {
113 * "3": 0.1,
114 * },
115 * },
116 * });
117 * ```
118 *
119 * ### Async Invocation
120 * **Example:** Alias-Scoped Retry Behavior
121 * ```typescript
122 * const version = yield* Version("LiveVersion", { function: fn });
123 * const alias = yield* Alias("LiveAlias", {
124 * version,
125 * aliasName: "live",
126 * eventInvokeConfig: {
127 * maximumRetryAttempts: 0,
128 * destinationConfig: {
129 * OnFailure: {
130 * Destination: queue.queueArn,
131 * },
132 * },
133 * },
134 * });
135 * ```
136 */
137export const Alias = Resource<Alias>("AWS.Lambda.Alias");
138
139const normalizeRoutingConfig = (
140 config: Lambda.AliasRoutingConfiguration | undefined,
141): Lambda.AliasRoutingConfiguration | undefined => {
142 const weights = Object.fromEntries(
143 Object.entries(config?.AdditionalVersionWeights ?? {}).filter(
144 (entry): entry is [string, number] => entry[1] !== undefined,
145 ),
146 );
147 return Object.keys(weights).length > 0
148 ? { AdditionalVersionWeights: weights }
149 : undefined;
150};
151
152export const AliasProvider = () =>
153 Provider.effect(
154 Alias,
155 Effect.gen(function* () {
156 const createAliasName = (id: string, aliasName?: string) =>
157 aliasName
158 ? Effect.succeed(aliasName)
159 : createPhysicalName({
160 id,
161 maxLength: 128,
162 delimiter: "-",
163 });
164
165 const retryOnAliasConflict = <A, E extends { _tag: string }, R>(
166 effect: Effect.Effect<A, E, R>,
167 ) =>
168 effect.pipe(
169 Effect.retry({
170 while: (e) => e._tag === "ResourceConflictException",
171 schedule: Schedule.max([
172 Schedule.exponential(500),
173 Schedule.recurs(10),
174 ]),
175 }),
176 );
177
178 const snapshotAlias = (
179 functionName: string,
180 alias: Lambda.AliasConfiguration,
181 region: string,
182 ): Alias["Attributes"] | undefined => {
183 if (!alias.AliasArn || !alias.Name || !alias.FunctionVersion) {
184 return undefined;
185 }
186 return {
187 aliasArn: alias.AliasArn,
188 aliasName: alias.Name,
189 functionName,
190 functionVersion: alias.FunctionVersion,
191 invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${alias.AliasArn}/invocations`,
192 description: alias.Description || undefined,
193 routingConfig: normalizeRoutingConfig(alias.RoutingConfig),
194 revisionId: alias.RevisionId,
195 };
196 };
197
198 return {
199 stables: ["aliasArn", "aliasName", "functionName"],
200 diff: Effect.fn(function* ({ id, olds, news }) {
201 if (!isResolved(news)) return;
202 const resolvedOlds = resolvedProps(olds);
203 const resolvedNews = resolvedProps(news);
204 const oldAliasName = yield* createAliasName(
205 id,
206 resolvedOlds.aliasName,
207 );
208 const newAliasName = yield* createAliasName(
209 id,
210 resolvedNews.aliasName,
211 );
212 if (
213 resolvedOlds.version.functionName !==
214 resolvedNews.version.functionName ||
215 oldAliasName !== newAliasName
216 ) {
217 return { action: "replace" } as const;
218 }
219 }),
220 read: Effect.fn(function* ({ id, olds, output }) {
221 const functionName =
222 output?.functionName ??
223 (olds ? resolvedProps(olds).version.functionName : undefined);
224 if (!functionName) return undefined;
225 const aliasName =
226 output?.aliasName ??
227 (yield* createAliasName(
228 id,
229 olds ? resolvedProps(olds).aliasName : undefined,
230 ));
231 const { region } = yield* AWSEnvironment.current;
232 const alias = yield* Lambda.getAlias({
233 FunctionName: functionName,
234 Name: aliasName,
235 }).pipe(
236 Effect.catchTag("ResourceNotFoundException", () =>
237 Effect.succeed(undefined),
238 ),
239 );
240 return alias ? snapshotAlias(functionName, alias, region) : undefined;
241 }),
242 list: () =>
243 Effect.gen(function* () {
244 const { region } = yield* AWSEnvironment.current;
245 const functionNames = yield* Lambda.listFunctions.pages({}).pipe(
246 Stream.runCollect,
247 Effect.map((chunk) =>
248 Array.from(chunk).flatMap((page) =>
249 (page.Functions ?? [])
250 .map((fn) => fn.FunctionName)
251 .filter((name): name is string => name !== undefined),
252 ),
253 ),
254 );
255 const aliases = yield* Effect.forEach(
256 functionNames,
257 (functionName) =>
258 Lambda.listAliases.items({ FunctionName: functionName }).pipe(
259 Stream.runCollect,
260 Effect.map((chunk) =>
261 Array.from(chunk).flatMap((alias) => {
262 const attrs = snapshotAlias(functionName, alias, region);
263 return attrs ? [attrs] : [];
264 }),
265 ),
266 Effect.catchTag("ResourceNotFoundException", () =>
267 Effect.succeed([] as Alias["Attributes"][]),
268 ),
269 ),
270 { concurrency: 10 },
271 );
272 return aliases.flat();
273 }),
274 reconcile: Effect.fn(function* ({ id, news, output, session }) {
275 const resolvedNews = resolvedProps(news);
276 const functionName = resolvedNews.version.functionName;
277 const functionVersion = resolvedNews.version.version;
278 const { region } = yield* AWSEnvironment.current;
279 const aliasName =
280 output?.aliasName ??
281 (yield* createAliasName(id, resolvedNews.aliasName));
282 const desiredRoutingConfig = normalizeRoutingConfig(
283 resolvedNews.routingConfig,
284 );
285 const getAlias = Lambda.getAlias({
286 FunctionName: functionName,
287 Name: aliasName,
288 }).pipe(
289 Effect.catchTag("ResourceNotFoundException", () =>
290 Effect.succeed(undefined),
291 ),
292 );
293
294 let alias = yield* getAlias;
295
296 if (!alias) {
297 alias = yield* Lambda.createAlias({
298 FunctionName: functionName,
299 Name: aliasName,
300 FunctionVersion: functionVersion,
301 Description: resolvedNews.description,
302 RoutingConfig: desiredRoutingConfig,
303 }).pipe(
304 Effect.catchTag("ResourceConflictException", () => getAlias),
305 );
306 }
307
308 const observedRoutingConfig = normalizeRoutingConfig(
309 alias?.RoutingConfig,
310 );
311 if (
312 !alias ||
313 alias.FunctionVersion !== functionVersion ||
314 (alias.Description || undefined) !== resolvedNews.description ||
315 !deepEqual(observedRoutingConfig, desiredRoutingConfig)
316 ) {
317 alias = yield* retryOnAliasConflict(
318 Lambda.updateAlias({
319 FunctionName: functionName,
320 Name: aliasName,
321 FunctionVersion: functionVersion,
322 Description: resolvedNews.description ?? "",
323 RoutingConfig:
324 desiredRoutingConfig ??
325 (observedRoutingConfig
326 ? { AdditionalVersionWeights: {} }
327 : undefined),
328 }),
329 );
330 }
331
332 const attrs = snapshotAlias(functionName, alias, region);
333 if (!attrs) {
334 return yield* Effect.die(
335 `Lambda alias ${aliasName} did not return complete attributes.`,
336 );
337 }
338
339 yield* syncEventInvokeConfig({
340 functionName,
341 qualifier: attrs.aliasName,
342 config: resolvedNews.eventInvokeConfig,
343 });
344
345 yield* session.note(
346 `Alias ${attrs.aliasName} on ${attrs.functionName}`,
347 );
348
349 return attrs;
350 }),
351 delete: Effect.fn(function* ({ output }) {
352 // The alias-scoped async config does not die with the alias — clear
353 // it first so a later alias of the same name starts clean.
354 yield* syncEventInvokeConfig({
355 functionName: output.functionName,
356 qualifier: output.aliasName,
357 config: undefined,
358 });
359 yield* retryOnAliasConflict(
360 Lambda.deleteAlias({
361 FunctionName: output.functionName,
362 Name: output.aliasName,
363 }),
364 ).pipe(
365 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
366 );
367 }),
368 };
369 }),
370 );