Skip to content

Commit

Permalink
feat(lambda): function URLs (aws#19817)
Browse files Browse the repository at this point in the history
feat(aws-lambda): Add support for function URL

closes aws#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*
  • Loading branch information
ayush987goyal authored and Stephen Potter committed Apr 27, 2022
1 parent f8bf836 commit b5fd83e
Show file tree
Hide file tree
Showing 17 changed files with 896 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
public grantInvoke(identity: iam.IGrantable): iam.Grant {
return this.lambda.grantInvoke(identity);
}
public grantInvokeUrl(identity: iam.IGrantable): iam.Grant {
return this.lambda.grantInvokeUrl(identity);
}
public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metric(metricName, { ...props, region: EdgeFunction.EDGE_REGION });
}
Expand All @@ -137,6 +140,9 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
public configureAsyncInvoke(options: lambda.EventInvokeConfigOptions): void {
return this.lambda.configureAsyncInvoke(options);
}
public addFunctionUrl(options?: lambda.FunctionUrlOptions): lambda.FunctionUrl {
return this.lambda.addFunctionUrl(options);
}

/** Create a function in-region */
private createInRegionFunction(props: lambda.FunctionProps): FunctionConfig {
Expand Down
67 changes: 67 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,73 @@ const fn = new lambda.Function(this, 'MyFunction', {
fn.currentVersion.addAlias('live');
```

## Function URL

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).

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.

See the [Invoking Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html) section of the AWS Lambda Developer Guide
for more information on the input and output payloads of Functions invoked in this way.

### IAM-authenticated Function URLs

To create a Function URL which can be called by an IAM identity, call `addFunctionUrl()`, followed by `grantInvokeFunctionUrl()`:

```ts
// Can be a Function or an Alias
declare const fn: lambda.Function;
declare const myRole: iam.Role;

const fnUrl = fn.addFunctionUrl();
fnUrl.grantInvokeUrl(myRole);

new CfnOutput(this, 'TheUrl', {
// The .url attributes will return the unique Function URL
value: fnUrl.url,
});
```

Calls to this URL need to be signed with SigV4.

### Anonymous Function URLs

To create a Function URL which can be called anonymously, pass `authType: FunctionUrlAuthType.NONE` to `addFunctionUrl()`:

```ts
// Can be a Function or an Alias
declare const fn: lambda.Function;

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

new CfnOutput(this, 'TheUrl', {
value: fnUrl.url,
});
```

### CORS configuration for Function URLs

If you want your Function URLs to be invokable from a web page in browser, you
will need to configure cross-origin resource sharing to allow the call (if you do
not do this, your browser will refuse to make the call):

```ts
declare const fn: lambda.Function;

fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
cors: {
// Allow this to be called from websites on https://example.com.
// Can also be ['*'] to allow all domain.
allowedOrigins: ['https://example.com'],

// More options are possible here, see the documentation for FunctionUrlCorsOptions
},
});
```

## Layers

The `lambda.LayerVersion` class can be used to define Lambda layers and manage
Expand Down
113 changes: 84 additions & 29 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Architecture } from './architecture';
import { EventInvokeConfig, EventInvokeConfigOptions } from './event-invoke-config';
import { IEventSource } from './event-source';
import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping';
import { FunctionUrlAuthType, FunctionUrlOptions, FunctionUrl } from './function-url';
import { IVersion } from './lambda-version';
import { CfnPermission } from './lambda.generated';
import { Permission } from './permission';
Expand Down Expand Up @@ -98,6 +99,11 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
*/
grantInvoke(identity: iam.IGrantable): iam.Grant;

/**
* Grant the given identity permissions to invoke this Lambda Function URL
*/
grantInvokeUrl(identity: iam.IGrantable): iam.Grant;

/**
* Return the given named metric for this Lambda
*/
Expand Down Expand Up @@ -141,6 +147,11 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
* Configures options for asynchronous invocation.
*/
configureAsyncInvoke(options: EventInvokeConfigOptions): void;

/**
* Adds a url to this lambda function.
*/
addFunctionUrl(options?: FunctionUrlOptions): FunctionUrl;
}

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

/**
* Mapping of fucntion URL invocation principals to grants. Used to de-dupe `grantInvokeUrl()` calls.
* @internal
*/
protected _functionUrlInvocationGrants: Record<string, iam.Grant> = {};

/**
* A warning will be added to functions under the following conditions:
* - permissions that include `lambda:InvokeFunction` are added to the unqualified function.
Expand Down Expand Up @@ -342,6 +359,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
eventSourceToken: permission.eventSourceToken,
sourceAccount: permission.sourceAccount ?? sourceAccount,
sourceArn: permission.sourceArn ?? sourceArn,
functionUrlAuthType: permission.functionUrlAuthType,
});
}

Expand Down Expand Up @@ -401,40 +419,29 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
// Memoize the result so subsequent grantInvoke() calls are idempotent
let grant = this._invocationGrants[identifier];
if (!grant) {
grant = iam.Grant.addToPrincipalOrResource({
grantee,
actions: ['lambda:InvokeFunction'],
resourceArns: this.resourceArnsForGrantInvoke,

// Fake resource-like object on which to call addToResourcePolicy(), which actually
// calls addPermission()
resource: {
addToResourcePolicy: (_statement) => {
// Couldn't add permissions to the principal, so add them locally.
this.addPermission(identifier, {
principal: grantee.grantPrincipal!,
action: 'lambda:InvokeFunction',
});

const permissionNode = this._functionNode().tryFindChild(identifier);
if (!permissionNode && !this._skipPermissions) {
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n'
+ 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.');
}
return { statementAdded: true, policyDependable: permissionNode };
},
node: this.node,
stack: this.stack,
env: this.env,
applyRemovalPolicy: this.applyRemovalPolicy,
},
});
grant = this.grant(grantee, identifier, 'lambda:InvokeFunction', this.resourceArnsForGrantInvoke);
this._invocationGrants[identifier] = grant;
}
return grant;
}

/**
* Grant the given identity permissions to invoke this Lambda Function URL
*/
public grantInvokeUrl(grantee: iam.IGrantable): iam.Grant {
const identifier = `InvokeFunctionUrl${grantee.grantPrincipal}`; // calls the .toString() of the principal

// Memoize the result so subsequent grantInvoke() calls are idempotent
let grant = this._functionUrlInvocationGrants[identifier];
if (!grant) {
grant = this.grant(grantee, identifier, 'lambda:InvokeFunctionUrl', [this.functionArn], {
functionUrlAuthType: FunctionUrlAuthType.AWS_IAM,
});
this._functionUrlInvocationGrants[identifier] = grant;
}
return grant;
}

public addEventSource(source: IEventSource) {
source.bind(this);
}
Expand All @@ -450,6 +457,13 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
});
}

public addFunctionUrl(options?: FunctionUrlOptions): FunctionUrl {
return new FunctionUrl(this, 'FunctionUrl', {
function: this,
...options,
});
}

/**
* Returns the construct tree node that corresponds to the lambda function.
* For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction.
Expand Down Expand Up @@ -480,6 +494,47 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
return this.stack.splitArn(this.functionArn, ArnFormat.SLASH_RESOURCE_NAME).account === this.stack.account;
}

private grant(
grantee: iam.IGrantable,
identifier:string,
action: string,
resourceArns: string[],
permissionOverrides?: Partial<Permission>,
): iam.Grant {
const grant = iam.Grant.addToPrincipalOrResource({
grantee,
actions: [action],
resourceArns,

// Fake resource-like object on which to call addToResourcePolicy(), which actually
// calls addPermission()
resource: {
addToResourcePolicy: (_statement) => {
// Couldn't add permissions to the principal, so add them locally.
this.addPermission(identifier, {
principal: grantee.grantPrincipal!,
action: action,
...permissionOverrides,
});

const permissionNode = this._functionNode().tryFindChild(identifier);
if (!permissionNode && !this._skipPermissions) {
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n'
+ 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.');
}
return { statementAdded: true, policyDependable: permissionNode };
},
node: this.node,
stack: this.stack,
env: this.env,
applyRemovalPolicy: this.applyRemovalPolicy,
},
});

return grant;
}

/**
* Translate IPrincipal to something we can pass to AWS::Lambda::Permissions
*
Expand Down
Loading

0 comments on commit b5fd83e

Please sign in to comment.