Skip to content

Commit

Permalink
refactor(lambda): Standardize Lambda API (#2876)
Browse files Browse the repository at this point in the history
`Alias`es and `Version`s are now `IFunctions`, and other fixes.

BREAKING CHANGE:
- Renamed `Function.addLayer` to `addLayers` and made it variadic
- Removed `IFunction.handler` property
- Removed `IVersion.versionArn` property (the value is at `functionArn`)
- Removed `SingletonLayerVersion`
- Stopped exporting `LogRetention`
  • Loading branch information
RomainMuller authored Jun 17, 2019
1 parent 118a716 commit 6446b78
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 174 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ export class CloudFrontWebDistribution extends cdk.Construct implements IDistrib
lambdaFunctionAssociations: input.lambdaFunctionAssociations
.map(fna => ({
eventType: fna.eventType,
lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.versionArn,
lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.functionArn,
}))
});
}
Expand Down
67 changes: 49 additions & 18 deletions packages/@aws-cdk/aws-lambda/lib/alias.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import { Construct, Stack } from '@aws-cdk/cdk';
import { FunctionBase, IFunction } from './function-base';
import { IFunction, QualifiedFunctionBase } from './function-base';
import { IVersion } from './lambda-version';
import { CfnAlias } from './lambda.generated';

export interface IAlias extends IFunction {
/**
* Name of this alias.
*
* @attribute
*/
readonly aliasName: string;

/**
* The underlying Lambda function version.
*/
readonly version: IVersion;
}

/**
* Properties for a new Lambda alias
*/
Expand Down Expand Up @@ -47,10 +61,30 @@ export interface AliasProps {
readonly additionalVersions?: VersionWeight[];
}

export interface AliasAttributes {
readonly aliasName: string;
readonly aliasVersion: IVersion;
}

/**
* A new alias to a particular version of a Lambda function.
*/
export class Alias extends FunctionBase {
export class Alias extends QualifiedFunctionBase implements IAlias {
public static fromAliasAttributes(scope: Construct, id: string, attrs: AliasAttributes): IAlias {
class Imported extends QualifiedFunctionBase implements IAlias {
public readonly aliasName = attrs.aliasName;
public readonly version = attrs.aliasVersion;
public readonly lambda = attrs.aliasVersion.lambda;
public readonly functionArn = `${attrs.aliasVersion.lambda.functionArn}:${attrs.aliasName}`;
public readonly functionName = `${attrs.aliasVersion.lambda.functionName}:${attrs.aliasName}`;
public readonly grantPrincipal = attrs.aliasVersion.grantPrincipal;
public readonly role = attrs.aliasVersion.role;

protected readonly canCreatePermissions = false;
}
return new Imported(scope, id);
}

/**
* Name of this alias.
*
Expand All @@ -65,6 +99,10 @@ export class Alias extends FunctionBase {
*/
public readonly functionName: string;

public readonly lambda: IFunction;

public readonly version: IVersion;

/**
* ARN of this alias
*
Expand All @@ -75,21 +113,17 @@ export class Alias extends FunctionBase {

protected readonly canCreatePermissions: boolean = true;

/**
* The actual Lambda function object that this Alias is pointing to
*/
private readonly underlyingLambda: IFunction;

constructor(scope: Construct, id: string, props: AliasProps) {
super(scope, id);

this.lambda = props.version.lambda;
this.aliasName = props.aliasName;
this.underlyingLambda = props.version.lambda;
this.version = props.version;

const alias = new CfnAlias(this, 'Resource', {
name: props.aliasName,
description: props.description,
functionName: this.underlyingLambda.functionName,
functionName: this.version.lambda.functionName,
functionVersion: props.version.version,
routingConfig: this.determineRoutingConfig(props)
});
Expand All @@ -101,26 +135,23 @@ export class Alias extends FunctionBase {
this.functionArn = alias.aliasArn;
}

/**
* Role associated with this alias
*/
public get role() {
return this.underlyingLambda.role;
public get grantPrincipal() {
return this.version.grantPrincipal;
}

public get grantPrincipal() {
return this.underlyingLambda.grantPrincipal;
public get role() {
return this.version.role;
}

public metric(metricName: string, props: cloudwatch.MetricOptions = {}): cloudwatch.Metric {
// Metrics on Aliases need the "bare" function name, and the alias' ARN, this differes from the base behavior.
return super.metric(metricName, {
dimensions: {
FunctionName: this.underlyingLambda.functionName,
FunctionName: this.lambda.functionName,
// construct the ARN from the underlying lambda so that alarms on an alias
// don't cause a circular dependency with CodeDeploy
// see: https://github.com/awslabs/aws-cdk/issues/2231
Resource: `${this.underlyingLambda.functionArn}:${this.aliasName}`
Resource: `${this.lambda.functionArn}:${this.aliasName}`
},
...props
});
Expand Down
53 changes: 37 additions & 16 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import { Permission } from './permission';

export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {

/**
* Logical ID of this Function.
*/
readonly id: string;

/**
* The name of the function.
*
Expand Down Expand Up @@ -122,7 +117,7 @@ export interface FunctionAttributes {
readonly securityGroupId?: string;
}

export abstract class FunctionBase extends Resource implements IFunction {
export abstract class FunctionBase extends Resource implements IFunction {
/**
* The principal this Lambda Function is running as
*/
Expand All @@ -145,11 +140,6 @@ export abstract class FunctionBase extends Resource implements IFunction {
*/
public abstract readonly role?: iam.IRole;

/**
* The $LATEST version of this function.
*/
public readonly latestVersion: IVersion = new LatestVersion(this);

/**
* Whether the addPermission() call adds any permissions
*
Expand Down Expand Up @@ -188,10 +178,6 @@ export abstract class FunctionBase extends Resource implements IFunction {
});
}

public get id() {
return this.node.id;
}

public addToRolePolicy(statement: iam.PolicyStatement) {
if (!this.role) {
return;
Expand All @@ -213,6 +199,11 @@ export abstract class FunctionBase extends Resource implements IFunction {
return this._connections;
}

public get latestVersion(): IVersion {
// Dynamic to avoid invinite recursion when creating the LatestVersion instance...
return new LatestVersion(this);
}

/**
* Whether or not this Lambda function was bound to a VPC
*
Expand Down Expand Up @@ -289,15 +280,45 @@ export abstract class FunctionBase extends Resource implements IFunction {
}
}

export abstract class QualifiedFunctionBase extends FunctionBase {
public abstract readonly lambda: IFunction;

public get latestVersion() {
return this.lambda.latestVersion;
}
}

/**
* The $LATEST version of a function, useful when attempting to create aliases.
*/
class LatestVersion extends Resource implements IVersion {
class LatestVersion extends FunctionBase implements IVersion {
public readonly lambda: IFunction;
public readonly version = '$LATEST';

protected readonly canCreatePermissions = true;

constructor(lambda: FunctionBase) {
super(lambda, '$LATEST');
this.lambda = lambda;
}

public get functionArn() {
return `${this.lambda.functionArn}:${this.version}`;
}

public get functionName() {
return `${this.lambda.functionName}:${this.version}`;
}

public get grantPrincipal() {
return this.lambda.grantPrincipal;
}

public get latestVersion() {
return this;
}

public get role() {
return this.lambda.role;
}
}
54 changes: 15 additions & 39 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ export class Function extends FunctionBase {
class Import extends FunctionBase {
public readonly functionName = functionName;
public readonly functionArn = functionArn;
public readonly role = role;
public readonly grantPrincipal: iam.IPrincipal;
public readonly role = role;

protected readonly canCreatePermissions = false;

Expand Down Expand Up @@ -370,11 +370,6 @@ export class Function extends FunctionBase {
*/
public readonly runtime: Runtime;

/**
* The name of the handler configured for this lambda.
*/
public readonly handler: string;

/**
* The principal this Lambda Function is running as
*/
Expand Down Expand Up @@ -442,14 +437,13 @@ export class Function extends FunctionBase {

this.functionName = resource.refAsString;
this.functionArn = resource.functionArn;
this.handler = props.handler;
this.runtime = props.runtime;

// allow code to bind to stack.
props.code.bind(this);

for (const layer of props.layers || []) {
this.addLayer(layer);
if (props.layers) {
this.addLayers(...props.layers);
}

for (const event of props.events || []) {
Expand Down Expand Up @@ -481,22 +475,23 @@ export class Function extends FunctionBase {
}

/**
* Adds a Lambda Layer to this Lambda function.
* Adds one or more Lambda Layers to this Lambda function.
*
* @param layer the layer to be added.
* @param layers the layers to be added.
*
* @throws if there are already 5 layers on this function, or the layer is incompatible with this function's runtime.
*/
public addLayer(layer: ILayerVersion): this {
if (this.layers.length === 5) {
throw new Error('Unable to add layer: this lambda function already uses 5 layers.');
}
if (layer.compatibleRuntimes && !layer.compatibleRuntimes.find(runtime => runtime.runtimeEquals(this.runtime))) {
const runtimes = layer.compatibleRuntimes.map(runtime => runtime.name).join(', ');
throw new Error(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`);
public addLayers(...layers: ILayerVersion[]): void {
for (const layer of layers) {
if (this.layers.length === 5) {
throw new Error('Unable to add layer: this lambda function already uses 5 layers.');
}
if (layer.compatibleRuntimes && !layer.compatibleRuntimes.find(runtime => runtime.runtimeEquals(this.runtime))) {
const runtimes = layer.compatibleRuntimes.map(runtime => runtime.name).join(', ');
throw new Error(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`);
}
this.layers.push(layer);
}
this.layers.push(layer);
return this;
}

/**
Expand All @@ -523,25 +518,6 @@ export class Function extends FunctionBase {
});
}

/**
* Add a new version for this Lambda, always with a different name.
*
* This is similar to the {@link addVersion} method,
* but useful when deploying this Lambda through CodePipeline with blue/green deployments.
* When using {@link addVersion},
* your Alias will not be updated until you change the name passed to {@link addVersion} in your CDK code.
* When deploying through a Pipeline,
* that might lead to a situation where a change to your Lambda application code will never be activated,
* even though it traveled through the entire Pipeline,
* because the Alias is still pointing to an old Version.
* This method creates a new, unique Version every time the CDK code is executed,
* and so prevents that from happening.
*/
public newVersion(): Version {
const now = new Date();
return this.addVersion(now.toISOString());
}

private renderEnvironment() {
if (!this.environment || Object.keys(this.environment).length === 0) {
return undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-lambda/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './lambda-version';
export * from './singleton-lambda';
export * from './event-source';
export * from './event-source-mapping';

export * from './log-retention';

// AWS::Lambda CloudFormation Resources:
Expand Down
Loading

0 comments on commit 6446b78

Please sign in to comment.