Skip to content

Commit 4fd515a

Browse files
feat(lambda): function URLs (#19817)
feat(aws-lambda): Add support for function URL closes #19798 Refs: 1. https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html#urls-cfn 2. https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunctionUrlConfig.html ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [x] Did you use `cdk-integ` to deploy the infrastructure and generate the snapshot (i.e. `cdk-integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d4373d7 commit 4fd515a

File tree

17 files changed

+896
-32
lines changed

17 files changed

+896
-32
lines changed

packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
115115
public grantInvoke(identity: iam.IGrantable): iam.Grant {
116116
return this.lambda.grantInvoke(identity);
117117
}
118+
public grantInvokeUrl(identity: iam.IGrantable): iam.Grant {
119+
return this.lambda.grantInvokeUrl(identity);
120+
}
118121
public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
119122
return this.lambda.metric(metricName, { ...props, region: EdgeFunction.EDGE_REGION });
120123
}
@@ -137,6 +140,9 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
137140
public configureAsyncInvoke(options: lambda.EventInvokeConfigOptions): void {
138141
return this.lambda.configureAsyncInvoke(options);
139142
}
143+
public addFunctionUrl(options?: lambda.FunctionUrlOptions): lambda.FunctionUrl {
144+
return this.lambda.addFunctionUrl(options);
145+
}
140146

141147
/** Create a function in-region */
142148
private createInRegionFunction(props: lambda.FunctionProps): FunctionConfig {

packages/@aws-cdk/aws-lambda/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,73 @@ const fn = new lambda.Function(this, 'MyFunction', {
339339
fn.currentVersion.addAlias('live');
340340
```
341341

342+
## Function URL
343+
344+
A function URL is a dedicated HTTP(S) endpoint for your Lambda function. When you create a function URL, Lambda automatically generates a unique URL endpoint for you. Function URLs can be created for the latest version Lambda Functions, or Function Aliases (but not for Versions).
345+
346+
Function URLs are dual stack-enabled, supporting IPv4 and IPv6, and cross-origin resource sharing (CORS) configuration. After you configure a function URL for your function, you can invoke your function through its HTTP(S) endpoint via a web browser, curl, Postman, or any HTTP client. To invoke a function using IAM authentication your HTTP client must support SigV4 signing.
347+
348+
See the [Invoking Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html) section of the AWS Lambda Developer Guide
349+
for more information on the input and output payloads of Functions invoked in this way.
350+
351+
### IAM-authenticated Function URLs
352+
353+
To create a Function URL which can be called by an IAM identity, call `addFunctionUrl()`, followed by `grantInvokeFunctionUrl()`:
354+
355+
```ts
356+
// Can be a Function or an Alias
357+
declare const fn: lambda.Function;
358+
declare const myRole: iam.Role;
359+
360+
const fnUrl = fn.addFunctionUrl();
361+
fnUrl.grantInvokeUrl(myRole);
362+
363+
new CfnOutput(this, 'TheUrl', {
364+
// The .url attributes will return the unique Function URL
365+
value: fnUrl.url,
366+
});
367+
```
368+
369+
Calls to this URL need to be signed with SigV4.
370+
371+
### Anonymous Function URLs
372+
373+
To create a Function URL which can be called anonymously, pass `authType: FunctionUrlAuthType.NONE` to `addFunctionUrl()`:
374+
375+
```ts
376+
// Can be a Function or an Alias
377+
declare const fn: lambda.Function;
378+
379+
const fnUrl = fn.addFunctionUrl({
380+
authType: lambda.FunctionUrlAuthType.NONE,
381+
});
382+
383+
new CfnOutput(this, 'TheUrl', {
384+
value: fnUrl.url,
385+
});
386+
```
387+
388+
### CORS configuration for Function URLs
389+
390+
If you want your Function URLs to be invokable from a web page in browser, you
391+
will need to configure cross-origin resource sharing to allow the call (if you do
392+
not do this, your browser will refuse to make the call):
393+
394+
```ts
395+
declare const fn: lambda.Function;
396+
397+
fn.addFunctionUrl({
398+
authType: lambda.FunctionUrlAuthType.NONE,
399+
cors: {
400+
// Allow this to be called from websites on https://example.com.
401+
// Can also be ['*'] to allow all domain.
402+
allowedOrigins: ['https://example.com'],
403+
404+
// More options are possible here, see the documentation for FunctionUrlCorsOptions
405+
},
406+
});
407+
```
408+
342409
## Layers
343410

344411
The `lambda.LayerVersion` class can be used to define Lambda layers and manage

packages/@aws-cdk/aws-lambda/lib/function-base.ts

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Architecture } from './architecture';
77
import { EventInvokeConfig, EventInvokeConfigOptions } from './event-invoke-config';
88
import { IEventSource } from './event-source';
99
import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping';
10+
import { FunctionUrlAuthType, FunctionUrlOptions, FunctionUrl } from './function-url';
1011
import { IVersion } from './lambda-version';
1112
import { CfnPermission } from './lambda.generated';
1213
import { Permission } from './permission';
@@ -98,6 +99,11 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
9899
*/
99100
grantInvoke(identity: iam.IGrantable): iam.Grant;
100101

102+
/**
103+
* Grant the given identity permissions to invoke this Lambda Function URL
104+
*/
105+
grantInvokeUrl(identity: iam.IGrantable): iam.Grant;
106+
101107
/**
102108
* Return the given named metric for this Lambda
103109
*/
@@ -141,6 +147,11 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
141147
* Configures options for asynchronous invocation.
142148
*/
143149
configureAsyncInvoke(options: EventInvokeConfigOptions): void;
150+
151+
/**
152+
* Adds a url to this lambda function.
153+
*/
154+
addFunctionUrl(options?: FunctionUrlOptions): FunctionUrl;
144155
}
145156

146157
/**
@@ -290,6 +301,12 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
290301
*/
291302
protected _invocationGrants: Record<string, iam.Grant> = {};
292303

304+
/**
305+
* Mapping of fucntion URL invocation principals to grants. Used to de-dupe `grantInvokeUrl()` calls.
306+
* @internal
307+
*/
308+
protected _functionUrlInvocationGrants: Record<string, iam.Grant> = {};
309+
293310
/**
294311
* A warning will be added to functions under the following conditions:
295312
* - permissions that include `lambda:InvokeFunction` are added to the unqualified function.
@@ -342,6 +359,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
342359
eventSourceToken: permission.eventSourceToken,
343360
sourceAccount: permission.sourceAccount ?? sourceAccount,
344361
sourceArn: permission.sourceArn ?? sourceArn,
362+
functionUrlAuthType: permission.functionUrlAuthType,
345363
});
346364
}
347365

@@ -401,40 +419,29 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
401419
// Memoize the result so subsequent grantInvoke() calls are idempotent
402420
let grant = this._invocationGrants[identifier];
403421
if (!grant) {
404-
grant = iam.Grant.addToPrincipalOrResource({
405-
grantee,
406-
actions: ['lambda:InvokeFunction'],
407-
resourceArns: this.resourceArnsForGrantInvoke,
408-
409-
// Fake resource-like object on which to call addToResourcePolicy(), which actually
410-
// calls addPermission()
411-
resource: {
412-
addToResourcePolicy: (_statement) => {
413-
// Couldn't add permissions to the principal, so add them locally.
414-
this.addPermission(identifier, {
415-
principal: grantee.grantPrincipal!,
416-
action: 'lambda:InvokeFunction',
417-
});
418-
419-
const permissionNode = this._functionNode().tryFindChild(identifier);
420-
if (!permissionNode && !this._skipPermissions) {
421-
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
422-
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n'
423-
+ 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.');
424-
}
425-
return { statementAdded: true, policyDependable: permissionNode };
426-
},
427-
node: this.node,
428-
stack: this.stack,
429-
env: this.env,
430-
applyRemovalPolicy: this.applyRemovalPolicy,
431-
},
432-
});
422+
grant = this.grant(grantee, identifier, 'lambda:InvokeFunction', this.resourceArnsForGrantInvoke);
433423
this._invocationGrants[identifier] = grant;
434424
}
435425
return grant;
436426
}
437427

428+
/**
429+
* Grant the given identity permissions to invoke this Lambda Function URL
430+
*/
431+
public grantInvokeUrl(grantee: iam.IGrantable): iam.Grant {
432+
const identifier = `InvokeFunctionUrl${grantee.grantPrincipal}`; // calls the .toString() of the principal
433+
434+
// Memoize the result so subsequent grantInvoke() calls are idempotent
435+
let grant = this._functionUrlInvocationGrants[identifier];
436+
if (!grant) {
437+
grant = this.grant(grantee, identifier, 'lambda:InvokeFunctionUrl', [this.functionArn], {
438+
functionUrlAuthType: FunctionUrlAuthType.AWS_IAM,
439+
});
440+
this._functionUrlInvocationGrants[identifier] = grant;
441+
}
442+
return grant;
443+
}
444+
438445
public addEventSource(source: IEventSource) {
439446
source.bind(this);
440447
}
@@ -450,6 +457,13 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
450457
});
451458
}
452459

460+
public addFunctionUrl(options?: FunctionUrlOptions): FunctionUrl {
461+
return new FunctionUrl(this, 'FunctionUrl', {
462+
function: this,
463+
...options,
464+
});
465+
}
466+
453467
/**
454468
* Returns the construct tree node that corresponds to the lambda function.
455469
* For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction.
@@ -480,6 +494,47 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
480494
return this.stack.splitArn(this.functionArn, ArnFormat.SLASH_RESOURCE_NAME).account === this.stack.account;
481495
}
482496

497+
private grant(
498+
grantee: iam.IGrantable,
499+
identifier:string,
500+
action: string,
501+
resourceArns: string[],
502+
permissionOverrides?: Partial<Permission>,
503+
): iam.Grant {
504+
const grant = iam.Grant.addToPrincipalOrResource({
505+
grantee,
506+
actions: [action],
507+
resourceArns,
508+
509+
// Fake resource-like object on which to call addToResourcePolicy(), which actually
510+
// calls addPermission()
511+
resource: {
512+
addToResourcePolicy: (_statement) => {
513+
// Couldn't add permissions to the principal, so add them locally.
514+
this.addPermission(identifier, {
515+
principal: grantee.grantPrincipal!,
516+
action: action,
517+
...permissionOverrides,
518+
});
519+
520+
const permissionNode = this._functionNode().tryFindChild(identifier);
521+
if (!permissionNode && !this._skipPermissions) {
522+
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
523+
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n'
524+
+ 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.');
525+
}
526+
return { statementAdded: true, policyDependable: permissionNode };
527+
},
528+
node: this.node,
529+
stack: this.stack,
530+
env: this.env,
531+
applyRemovalPolicy: this.applyRemovalPolicy,
532+
},
533+
});
534+
535+
return grant;
536+
}
537+
483538
/**
484539
* Translate IPrincipal to something we can pass to AWS::Lambda::Permissions
485540
*

0 commit comments

Comments
 (0)