Skip to content

Commit

Permalink
feat(lambda): cloudwatch lambda insights (#15439)
Browse files Browse the repository at this point in the history
Closes #12489

This allows a user to configure Lambda Insights and have the relevant IAM policy be added to the Lambda role.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
flemjame-at-amazon authored Jul 29, 2021
1 parent 6244a81 commit 9efd800
Show file tree
Hide file tree
Showing 14 changed files with 1,349 additions and 5 deletions.
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,28 @@ new LayerVersion(this, 'MyLayer', {
});
```

## Lambda Insights

Lambda functions can be configured to use CloudWatch [Lambda Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights.html)
which provides low-level runtime metrics for a Lambda functions.

```ts
import * as lambda from '@aws-cdk/lambda';

new Function(this, 'MyFunction', {
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0
})
```

If the version of insights is not yet available in the CDK, you can also provide the ARN directly as so -

```ts
const layerArn = 'arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:14';
new Function(this, 'MyFunction', {
insightsVersion: lambda.LambdaInsightsVersion.fromInsightVersionArn(layerArn)
})
```

## Event Rule Target

You can use an AWS Lambda function as a target for an Amazon CloudWatch event
Expand Down
29 changes: 27 additions & 2 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { FileSystem } from './filesystem';
import { FunctionAttributes, FunctionBase, IFunction } from './function-base';
import { calculateFunctionHash, trimFromStart } from './function-hash';
import { Handler } from './handler';
import { LambdaInsightsVersion } from './lambda-insights';
import { Version, VersionOptions } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
import { LayerVersion, ILayerVersion } from './layers';
import { Runtime } from './runtime';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -214,6 +215,14 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
*/
readonly profilingGroup?: IProfilingGroup;

/**
* Specify the version of CloudWatch Lambda insights to use for monitoring
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights.html
*
* @default - No Lambda Insights
*/
readonly insightsVersion?: LambdaInsightsVersion;

/**
* A list of layers to add to the function's execution environment. You can configure your Lambda function to pull in
* additional code during initialization in the form of layers. Layers are packages of libraries or other dependencies
Expand Down Expand Up @@ -649,7 +658,7 @@ export class Function extends FunctionBase {
zipFile: code.inlineCode,
imageUri: code.image?.imageUri,
},
layers: Lazy.list({ produce: () => this.layers.map(layer => layer.layerVersionArn) }, { omitEmpty: true }),
layers: Lazy.list({ produce: () => this.layers.map(layer => layer.layerVersionArn) }, { omitEmpty: true }), // Evaluated on synthesis
handler: props.handler === Handler.FROM_IMAGE ? undefined : props.handler,
timeout: props.timeout && props.timeout.toSeconds(),
packageType: props.runtime === Runtime.FROM_IMAGE ? 'Image' : undefined,
Expand Down Expand Up @@ -747,6 +756,11 @@ export class Function extends FunctionBase {
});
});
}

// Configure Lambda insights
if (props.insightsVersion !== undefined) {
this.configureLambdaInsights(props.insightsVersion);
}
}

/**
Expand Down Expand Up @@ -863,6 +877,17 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
return;
}

/**
* Configured lambda insights on the function if specified. This is acheived by adding an imported layer which is added to the
* list of lambda layers on synthesis.
*
* https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html
*/
private configureLambdaInsights(insightsVersion: LambdaInsightsVersion): void {
this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', insightsVersion.layerVersionArn));
this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'));
}

private renderEnvironment() {
if (!this.environment || Object.keys(this.environment).length === 0) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export * from './destination';
export * from './event-invoke-config';
export * from './scalable-attribute-api';
export * from './code-signing-config';

export * from './lambda-insights';
export * from './log-retention';

// AWS::Lambda CloudFormation Resources:
Expand Down
134 changes: 134 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/lambda-insights.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Aws, CfnMapping, Fn, IResolveContext, Lazy, Stack, Token } from '@aws-cdk/core';
import { FactName, RegionInfo } from '@aws-cdk/region-info';
import { CLOUDWATCH_LAMBDA_INSIGHTS_ARNS } from '@aws-cdk/region-info/build-tools/fact-tables';

// This is the name of the mapping that will be added to the CloudFormation template, if a stack is region agnostic
const DEFAULT_MAPPING_PREFIX = 'LambdaInsightsVersions';

// To add new versions, update fact-tables.ts `CLOUDWATCH_LAMBDA_INSIGHTS_ARNS` and create a new `public static readonly VERSION_A_B_C_D`

/**
* Version of CloudWatch Lambda Insights
*/
export abstract class LambdaInsightsVersion {

/**
* Version 1.0.54.0
*/
public static readonly VERSION_1_0_54_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.54.0');

/**
* Version 1.0.86.0
*/
public static readonly VERSION_1_0_86_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.86.0');

/**
* Version 1.0.89.0
*/
public static readonly VERSION_1_0_89_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.89.0');

/**
* Version 1.0.98.0
*/
public static readonly VERSION_1_0_98_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.98.0');

/**
* Use the insights extension associated with the provided ARN. Make sure the ARN is associated
* with same region as your function
*
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html
*/
public static fromInsightVersionArn(arn: string): LambdaInsightsVersion {
class InsightsArn extends LambdaInsightsVersion {
public readonly layerVersionArn = arn;
}
return new InsightsArn();
}

// Use the verison to build the object. Not meant to be called by the user -- user should use e.g. VERSION_1_0_54_0
private static fromInsightsVersion(insightsVersion: string): LambdaInsightsVersion {

// Check if insights version is valid. This should only happen if one of the public static readonly versions are set incorrectly
if (!(insightsVersion in CLOUDWATCH_LAMBDA_INSIGHTS_ARNS)) {
throw new Error(`Insights version ${insightsVersion} does not exist. Available versions are ${CLOUDWATCH_LAMBDA_INSIGHTS_ARNS.keys()}`);
}

class InsightsVersion extends LambdaInsightsVersion {
public readonly layerVersionArn = Lazy.uncachedString({
produce: (context) => getVersionArn(context, insightsVersion),
});
}
return new InsightsVersion();
}

/**
* The arn of the Lambda Insights extension
*/
public readonly layerVersionArn: string = '';
}

/**
* Function to retrieve the correct Lambda Insights ARN from RegionInfo,
* or create a mapping to look it up at stack deployment time.
*
* This function is run on CDK synthesis.
*/
function getVersionArn(context: IResolveContext, insightsVersion: string): string {

const scopeStack = Stack.of(context.scope);
const region = scopeStack.region;

// Region is defined, look up the arn, or throw an error if the version isn't supported by a region
if (region !== undefined && !Token.isUnresolved(region)) {
const arn = RegionInfo.get(region).cloudwatchLambdaInsightsArn(insightsVersion);
if (arn === undefined) {
throw new Error(`Insights version ${insightsVersion} is not supported in region ${region}`);
}
return arn;
}

// Otherwise, need to add a mapping to be looked up at deployment time

/**
* See this for the context as to why the mappings are the way they are
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html
*
* Mappings have to have a structure like this, and no functions can be used inside them:
* <Alphanumeric only>
* - <Can be non-alphanumeric>
* -- { <alphanumeric>: "value1"},
* -- { <alphanumeric>: "value2"}
*
* So we cannot have an otherwise ideal mapping like this, because '1.0.98.0' is non-alphanumeric:
* LambdaInsightsVersions
* - us-east-1
* -- {'1.0.98.0': 'arn1'},
* -- {'1.0.89.0': 'arn2'}
*
* To get around this limitation, this is the mapping structure:
* LambdaInsightsVersions10980 // for version 1.0.98.0
* - us-east-1
* -- {'arn': 'arn1'},
* - us-east-2
* -- {'arn': 'arn2'}
* LambdaInsightsVersions10890 // a separate mapping version 1.0.89.0
* - us-east-1
* -- {'arn': 'arn3'},
* - us-east-2
* -- {'arn': 'arn4'}
*/

const mapName = DEFAULT_MAPPING_PREFIX + insightsVersion.split('.').join('');
const mapping: { [k1: string]: { [k2: string]: any } } = {};
const region2arns = RegionInfo.regionMap(FactName.cloudwatchLambdaInsightsVersion(insightsVersion));
for (const [reg, arn] of Object.entries(region2arns)) {
mapping[reg] = { arn };
}

// Only create a given mapping once. If another version of insights is used elsewhere, that mapping will also exist
if (!scopeStack.node.tryFindChild(mapName)) {
new CfnMapping(scopeStack, mapName, { mapping });
}
// The ARN will be looked up at deployment time from the mapping we created
return Fn.findInMap(mapName, Aws.REGION, 'arn');
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"@aws-cdk/aws-sqs": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/region-info": "0.0.0",
"constructs": "^3.3.69"
},
"homepage": "https://github.com/aws/aws-cdk",
Expand All @@ -127,6 +128,7 @@
"@aws-cdk/aws-sqs": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/region-info": "0.0.0",
"constructs": "^3.3.69"
},
"engines": {
Expand Down
Loading

0 comments on commit 9efd800

Please sign in to comment.