Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lambda): currentVersion, version.addAlias() #6771

Merged
merged 34 commits into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9d6c458
feat(lambda): add alias to latest version through code hash
Mar 17, 2020
4ba3d1f
Update packages/@aws-cdk/aws-lambda/lib/code.ts
Mar 17, 2020
363e9a5
Merge remote-tracking branch 'origin/master' into benisrae/code-hash
Mar 22, 2020
e5b0e25
change approach to "currentVersion"
Mar 23, 2020
721f2d7
get rid of codeHash
Mar 23, 2020
da122b2
clean up
Mar 23, 2020
5e89466
doc fixes
Mar 23, 2020
411ffd8
fix lint breaks
Mar 23, 2020
e58a60a
fix snapshot
Mar 23, 2020
5a99536
add some docstrings
Mar 23, 2020
cfc0e38
Merge branch 'master' into benisrae/code-hash
Mar 23, 2020
02f2c7e
sort environment variables by key for stable hashing
Mar 24, 2020
73e38c1
fix typo
Mar 24, 2020
db61dcf
update snapthots
Mar 24, 2020
c7252fd
update snapshot
Mar 24, 2020
4df4fa3
Merge branch 'master' into benisrae/code-hash
Mar 24, 2020
32453ba
Update integ.lambda.expected.json
Mar 24, 2020
b9a21d7
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
6d2c1be
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
ee2b170
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
deef429
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
cf30ec0
Update test.code.ts
Mar 24, 2020
49457ba
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
629c81c
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
ac67061
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
5c804cd
do not set deletion policy if not specified
Mar 24, 2020
fe10bdb
Update integ.lambda.prov.concurrent.expected.json
Mar 24, 2020
e7bddd1
Merge branch 'master' into benisrae/code-hash
Mar 24, 2020
d240dfd
update snapshot
Mar 24, 2020
46366cb
update ddb snapshot
Mar 24, 2020
88787ff
revert location of logGroup
Mar 24, 2020
c045300
do not sort env vars if "currentVersion" is not used for backwards co…
Mar 25, 2020
3a06e76
revert newline
Mar 25, 2020
fba86fd
Merge branch 'master' into benisrae/code-hash
Mar 25, 2020
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
63 changes: 63 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,69 @@ to our CDK project directory. This is especially important when we want to share
this construct through a library. Different programming languages will have
different techniques for bundling resources into libraries.

When using `fromAsset` or `fromInline`, you can obtain the hash of source
through the `function.codeHash` property. This property will return `undefined`
if the code hash cannot be calculated during synthesis (e.g. when using code
from an S3 bucket).

### Versions and Aliases

You can use
[versions](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html)
to manage the deployment of your AWS Lambda functions. For example, you can
publish a new version of a function for beta testing without affecting users of
the stable production version.

The function version includes the following information:

- The function code and all associated dependencies.
- The Lambda runtime that executes the function.
- All of the function settings, including the environment variables.
- A unique Amazon Resource Name (ARN) to identify this version of the function.

You can define one or more
[aliases](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html)
for your AWS Lambda function. A Lambda alias is like a pointer to a specific
Lambda function version. Users can access the function version using the alias
ARN.

The `fn.currentVersion` property can be used to obtain a `lambda.Version`
resource that represents the AWS Lambda function defined in your application.
Any change to your function's code or configuration will result in the creation
of a new version resource. You can specify options for this version through the
`currentVersionOptions` property.

> The `currentVersion` property is only supported when your AWS Lambda function
> uses either `lambda.Code.fromAsset` or `lambda.Code.fromInline`. Other types
> of code providers (such as `lambda.Code.fromBucket`) require that you define a
> `lambda.Version` resource directly since the CDK is unable to determine if
> their contents had changed.

The `version.addAlias()` method can be used to define an AWS Lambda alias that
points to a specific version.

The following example defines an alias named `live` which will always point to a
version that represents the function as defined in your CDK app. When you change
your lambda code or configuration, a new resource will be created. You can
specify options for the current version through the `currentVersionOptions`
property.

```ts
const fn = new lambda.Function(this, 'MyFunction', {
currentVersionOptions: {
removalPolicy: RemovalPolicy.RETAIN, // retain old versions
retryAttempts: 1 // async retry attempts
}
});

fn.currentVersion.addAlias('live');
```

> NOTE: The `fn.latestVersion` property returns a `lambda.IVersion` which
> represents the `$LATEST` pseudo-version. Most AWS services require a specific
> AWS Lambda version, and won't allow you to use `$LATEST`. Therefore, you would
> normally want to use `lambda.currentVersion`.

### Layers

The `lambda.LayerVersion` class can be used to define Lambda layers and manage
Expand Down
33 changes: 19 additions & 14 deletions packages/@aws-cdk/aws-lambda/lib/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,16 @@ export interface IAlias extends IFunction {
}

/**
* Properties for a new Lambda alias
* Options for `lambda.Alias`.
*/
export interface AliasProps extends EventInvokeConfigOptions {
export interface AliasOptions extends EventInvokeConfigOptions {
/**
* Description for the alias
*
* @default No description
*/
readonly description?: string;

/**
* Function version this alias refers to
*
* Use lambda.addVersion() to obtain a new lambda version to refer to.
*/
readonly version: IVersion;

/**
* Name of this alias
*/
readonly aliasName: string;

/**
* Additional versions with individual weights this alias points to
*
Expand Down Expand Up @@ -69,6 +57,23 @@ export interface AliasProps extends EventInvokeConfigOptions {
readonly provisionedConcurrentExecutions?: number;
}

/**
* Properties for a new Lambda alias
*/
export interface AliasProps extends AliasOptions {
/**
* Name of this alias
*/
readonly aliasName: string;

/**
* Function version this alias refers to
*
* Use lambda.addVersion() to obtain a new lambda version to refer to.
*/
readonly version: IVersion;
}

export interface AliasAttributes {
readonly aliasName: string;
readonly aliasVersion: IVersion;
Expand Down
17 changes: 15 additions & 2 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import { ConstructNode, IResource, Resource } from '@aws-cdk/core';
import { AliasOptions } from './alias';
import { EventInvokeConfig, EventInvokeConfigOptions } from './event-invoke-config';
import { IEventSource } from './event-source';
import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping';
import { IVersion } from './lambda-version';
import { CfnPermission } from './lambda.generated';
import { Permission } from './permission';
import { addAlias } from './util';

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

Expand Down Expand Up @@ -39,6 +41,13 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {

/**
* The `$LATEST` version of this function.
*
* Note that this is reference to a non-specific AWS Lambda version, which
* means the function this version refers to can return different results in
* different invocations.
*
* To obtain a reference to an explicit version which references the current
* function configuration, use `lambdaFunction.currentVersion` instead.
*/
readonly latestVersion: IVersion;

Expand Down Expand Up @@ -102,7 +111,7 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
/**
* Configures options for asynchronous invocation.
*/
configureAsyncInvoke(options: EventInvokeConfigOptions): void
configureAsyncInvoke(options: EventInvokeConfigOptions): void;
}

/**
Expand Down Expand Up @@ -235,7 +244,7 @@ export abstract class FunctionBase extends Resource implements IFunction {
}

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

Expand Down Expand Up @@ -393,4 +402,8 @@ class LatestVersion extends FunctionBase implements IVersion {
public get role() {
return this.lambda.role;
}

public addAlias(aliasName: string, options: AliasOptions = {}) {
return addAlias(this, this, aliasName, options);
}
}
23 changes: 23 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/function-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CfnResource, Stack } from "@aws-cdk/core";
import * as crypto from 'crypto';
import { Function as LambdaFunction } from "./function";

export function calculateFunctionHash(fn: LambdaFunction) {
const stack = Stack.of(fn);

const functionResource = fn.node.defaultChild as CfnResource;

// render the cloudformation resource from this function
const config = stack.resolve((functionResource as any)._toCloudFormation());

const hash = crypto.createHash('md5');
hash.update(JSON.stringify(config));

return hash.digest('hex');
}

export function trimFromStart(s: string, maxLength: number) {
const desiredLength = Math.min(maxLength, s.length);
const newStart = s.length - desiredLength;
return s.substring(newStart);
}
103 changes: 90 additions & 13 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';
import * as sqs from '@aws-cdk/aws-sqs';
import { Construct, Duration, Fn, Lazy } from '@aws-cdk/core';
import { CfnResource, Construct, Duration, Fn, Lazy, Stack } from '@aws-cdk/core';
import { Code, CodeConfig } from './code';
import { EventInvokeConfigOptions } from './event-invoke-config';
import { IEventSource } from './event-source';
import { FunctionAttributes, FunctionBase, IFunction } from './function-base';
import { Version } from './lambda-version';
import { calculateFunctionHash, trimFromStart } from './function-hash';
import { Version, VersionOptions } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
import { LogRetention } from './log-retention';
Expand Down Expand Up @@ -224,6 +225,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
* @default - A new role is created.
*/
readonly logRetentionRole?: iam.IRole;

/**
* Options for the `lambda.Version` resource automatically created by the
* `fn.currentVersion` method.
* @default - default options as described in `VersionOptions`
*/
readonly currentVersionOptions?: VersionOptions;
}

export interface FunctionProps extends FunctionOptions {
Expand Down Expand Up @@ -266,6 +274,28 @@ export interface FunctionProps extends FunctionOptions {
* library.
*/
export class Function extends FunctionBase {

/**
* Returns a `lambda.Version` which represents the current version of this
* Lambda function. A new version will be created every time the function's
* configuration changes.
*
* You can specify options for this version using the `currentVersionOptions`
* prop when initializing the `lambda.Function`.
*/
public get currentVersion(): Version {
if (this._currentVersion) {
return this._currentVersion;
}

this._currentVersion = new Version(this, `CurrentVersion`, {
lambda: this,
...this.currentVersionOptions
});

return this._currentVersion;
}

public static fromFunctionArn(scope: Construct, id: string, functionArn: string): IFunction {
return Function.fromFunctionAttributes(scope, id, { functionArn });
}
Expand Down Expand Up @@ -425,6 +455,9 @@ export class Function extends FunctionBase {
*/
private readonly environment: { [key: string]: string };

private readonly currentVersionOptions?: VersionOptions;
private _currentVersion?: Version;

constructor(scope: Construct, id: string, props: FunctionProps) {
super(scope, id, {
physicalName: props.functionName,
Expand Down Expand Up @@ -520,6 +553,8 @@ export class Function extends FunctionBase {
retryAttempts: props.retryAttempts,
});
}

this.currentVersionOptions = props.currentVersionOptions;
}

/**
Expand Down Expand Up @@ -557,26 +592,35 @@ export class Function extends FunctionBase {
* Add a new version for this Lambda
*
* If you want to deploy through CloudFormation and use aliases, you need to
* add a new version (with a new name) to your Lambda every time you want
* to deploy an update. An alias can then refer to the newly created Version.
* add a new version (with a new name) to your Lambda every time you want to
* deploy an update. An alias can then refer to the newly created Version.
*
* All versions should have distinct names, and you should not delete versions
* as long as your Alias needs to refer to them.
*
* @param name A unique name for this version
* @param codeSha256 The SHA-256 hash of the most recently deployed Lambda source code, or
* omit to skip validation.
* @param name A unique name for this version.
* @param codeSha256 The SHA-256 hash of the most recently deployed Lambda
* source code, or omit to skip validation.
* @param description A description for this version.
* @param provisionedExecutions A provisioned concurrency configuration for a function's version.
* @param asyncInvokeConfig configuration for this version when it is invoked asynchronously.
* @param provisionedExecutions A provisioned concurrency configuration for a
* function's version.
* @param asyncInvokeConfig configuration for this version when it is invoked
* asynchronously.
* @returns A new Version object.
*
* @deprecated This method will create an AWS::Lambda::Version resource which
* snapshots the AWS Lambda function *at the time of its creation* and it
* won't get updated when the function changes. Instead, use
* `this.currentVersion` to obtain a reference to a version resource that gets
* automatically recreated when the function configuration (or code) changes.
*/
public addVersion(
name: string,
codeSha256?: string,
description?: string,
provisionedExecutions?: number,
asyncInvokeConfig: EventInvokeConfigOptions = {}): Version {

return new Version(this, 'Version' + name, {
lambda: this,
codeSha256,
Expand Down Expand Up @@ -607,14 +651,47 @@ export class Function extends FunctionBase {
return this._logGroup;
}

protected prepare() {
super.prepare();

// if we have a current version resource, override it's logical id
// so that it includes the hash of the function code and it's configuration.
if (this._currentVersion) {
const stack = Stack.of(this);
const cfn = this._currentVersion.node.defaultChild as CfnResource;
const originalLogicalId: string = stack.resolve(cfn.logicalId);

const hash = calculateFunctionHash(this);

const logicalId = trimFromStart(originalLogicalId, 255 - 32);
cfn.overrideLogicalId(`${logicalId}${hash}`);
}
}

private renderEnvironment() {
if (!this.environment || Object.keys(this.environment).length === 0) {
return undefined;
}

return {
variables: this.environment
};
// for backwards compatibility we do not sort environment variables in case
// _currentVersion is not defined. otherwise, this would have invalidated
// the template, and for example, may cause unneeded updates for nested
// stacks.
if (!this._currentVersion) {
return {
variables: this.environment
};
}

// sort environment so the hash of the function used to create
// `currentVersion` is not affected by key order (this is how lambda does
// it).
const variables: { [key: string]: string } = { };
for (const key of Object.keys(this.environment).sort()) {
variables[key] = this.environment[key];
}

return { variables };
}

/**
Expand Down Expand Up @@ -749,4 +826,4 @@ export function verifyCodeConfig(code: CodeConfig, runtime: Runtime) {
if (code.inlineCode && !runtime.supportsInlineCode) {
throw new Error(`Inline source not allowed for ${runtime.name}`);
}
}
}
Loading