Skip to content

Commit

Permalink
feat(lambda): add cloudwatch lambda insights arm support (#17665)
Browse files Browse the repository at this point in the history
Adding builtin support for the new ARM64 CloudWatch insights Lambda
layers which were [announced](https://aws.amazon.com/about-aws/whats-new/2021/11/amazon-cloudwatch-lambda-insights-functions-graviton2/)
yesterday.

also fixes #17133

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall authored Dec 13, 2021
1 parent 4937cd0 commit 02749b4
Show file tree
Hide file tree
Showing 17 changed files with 987 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
public readonly permissionsNode: ConstructNode;
public readonly role?: iam.IRole;
public readonly version: string;
public readonly architecture: lambda.Architecture;

private readonly _edgeFunction: lambda.Function;

Expand All @@ -66,6 +67,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
this.grantPrincipal = this._edgeFunction.role!;
this.permissionsNode = this._edgeFunction.permissionsNode;
this.version = lambda.extractQualifierFromArn(this.functionArn);
this.architecture = this._edgeFunction.architecture;

this.node.defaultChild = this._edgeFunction;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,19 @@ new lambda.Function(this, 'MyFunction', {
});
```

If you are deploying an ARM_64 Lambda Function, you must specify a
Lambda Insights Version >= `1_0_119_0`.

```ts
new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
architecture: lambda.Architecture.ARM_64,
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')),
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_119_0,
});
```

## Event Rule Target

You can use an AWS Lambda function as a target for an Amazon CloudWatch event
Expand Down
5 changes: 5 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as iam from '@aws-cdk/aws-iam';
import { ArnFormat } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Architecture } from './architecture';
import { EventInvokeConfigOptions } from './event-invoke-config';
import { IFunction, QualifiedFunctionBase } from './function-base';
import { extractQualifierFromArn, IVersion } from './lambda-version';
Expand Down Expand Up @@ -97,6 +98,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
public readonly functionName = `${attrs.aliasVersion.lambda.functionName}:${attrs.aliasName}`;
public readonly grantPrincipal = attrs.aliasVersion.grantPrincipal;
public readonly role = attrs.aliasVersion.role;
public readonly architecture = attrs.aliasVersion.lambda.architecture;

protected readonly canCreatePermissions = this._isStackAccount();
protected readonly qualifier = attrs.aliasName;
Expand All @@ -120,6 +122,8 @@ export class Alias extends QualifiedFunctionBase implements IAlias {

public readonly lambda: IFunction;

public readonly architecture: Architecture;

public readonly version: IVersion;

/**
Expand All @@ -145,6 +149,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
this.lambda = props.version.lambda;
this.aliasName = this.physicalName;
this.version = props.version;
this.architecture = this.lambda.architecture;

const alias = new CfnAlias(this, 'Resource', {
name: this.aliasName,
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import { ArnFormat, ConstructNode, IResource, Resource, Token } from '@aws-cdk/core';
import { AliasOptions } from './alias';
import { Architecture } from './architecture';
import { EventInvokeConfig, EventInvokeConfigOptions } from './event-invoke-config';
import { IEventSource } from './event-source';
import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping';
Expand Down Expand Up @@ -56,6 +57,11 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable {
*/
readonly permissionsNode: ConstructNode;

/**
* The system architectures compatible with this lambda function.
*/
readonly architecture: Architecture;

/**
* Adds an event source that maps to this AWS Lambda function.
* @param id construct ID
Expand Down Expand Up @@ -173,6 +179,12 @@ export interface FunctionAttributes {
* For environment-agnostic stacks this will default to `false`.
*/
readonly sameEnvironment?: boolean;

/**
* The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64).
* @default - Architecture.X86_64
*/
readonly architecture?: Architecture;
}

export abstract class FunctionBase extends Resource implements IFunction, ec2.IClientVpnConnectionHandler {
Expand Down Expand Up @@ -203,6 +215,11 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
*/
public abstract readonly permissionsNode: ConstructNode;

/**
* The architecture of this Lambda Function.
*/
public abstract readonly architecture: Architecture;

/**
* Whether the addPermission() call adds any permissions
*
Expand Down Expand Up @@ -521,6 +538,10 @@ class LatestVersion extends FunctionBase implements IVersion {
return `${this.lambda.functionName}:${this.version}`;
}

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

public get grantPrincipal() {
return this.lambda.grantPrincipal;
}
Expand Down
13 changes: 8 additions & 5 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export class Function extends FunctionBase {
public readonly grantPrincipal: iam.IPrincipal;
public readonly role = role;
public readonly permissionsNode = this.node;
public readonly architecture = attrs.architecture ?? Architecture.X86_64;

protected readonly canCreatePermissions = attrs.sameEnvironment ?? this._isStackAccount();

Expand Down Expand Up @@ -576,7 +577,7 @@ export class Function extends FunctionBase {
/**
* The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64).
*/
public readonly architecture?: Architecture;
public readonly architecture: Architecture;

/**
* The timeout configured for this lambda.
Expand All @@ -600,6 +601,8 @@ export class Function extends FunctionBase {
private readonly currentVersionOptions?: VersionOptions;
private _currentVersion?: Version;

private _architecture?: Architecture;

constructor(scope: Construct, id: string, props: FunctionProps) {
super(scope, id, {
physicalName: props.functionName,
Expand Down Expand Up @@ -683,7 +686,7 @@ export class Function extends FunctionBase {
if (props.architectures && props.architectures.length > 1) {
throw new Error('Only one architecture must be specified.');
}
const architecture = props.architecture ?? (props.architectures && props.architectures[0]);
this._architecture = props.architecture ?? (props.architectures && props.architectures[0]);

const resource: CfnFunction = new CfnFunction(this, 'Resource', {
functionName: this.physicalName,
Expand Down Expand Up @@ -717,7 +720,7 @@ export class Function extends FunctionBase {
kmsKeyArn: props.environmentEncryption?.keyArn,
fileSystemConfigs,
codeSigningConfigArn: props.codeSigningConfig?.codeSigningConfigArn,
architectures: architecture ? [architecture.name] : undefined,
architectures: this._architecture ? [this._architecture.name] : undefined,
});

resource.node.addDependency(this.role);
Expand All @@ -733,7 +736,7 @@ export class Function extends FunctionBase {
this.runtime = props.runtime;
this.timeout = props.timeout;

this.architecture = props.architecture;
this.architecture = props.architecture ?? Architecture.X86_64;

if (props.layers) {
if (props.runtime === Runtime.FROM_IMAGE) {
Expand Down Expand Up @@ -935,7 +938,7 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
if (props.runtime !== Runtime.FROM_IMAGE) {
// Layers cannot be added to Lambda container images. The image should have the insights agent installed.
// See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html
this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', props.insightsVersion.layerVersionArn));
this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', props.insightsVersion._bind(this, this).arn));
}
this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'));
}
Expand Down
77 changes: 65 additions & 12 deletions packages/@aws-cdk/aws-lambda/lib/lambda-insights.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { Aws, CfnMapping, Fn, IResolveContext, Lazy, Stack, Token } from '@aws-cdk/core';
import { FactName, RegionInfo } from '@aws-cdk/region-info';
import { Construct } from 'constructs';
import { Architecture } from './architecture';
import { IFunction } from './function-base';


// 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';

/**
* Config returned from {@link LambdaInsightsVersion._bind}
*/
interface InsightsBindConfig {
/**
* ARN of the Lambda Insights Layer Version
*/
readonly arn: string;
}

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

/**
Expand Down Expand Up @@ -31,6 +45,11 @@ export abstract class LambdaInsightsVersion {
*/
public static readonly VERSION_1_0_98_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.98.0');

/**
* Version 1.0.119.0
*/
public static readonly VERSION_1_0_119_0 = LambdaInsightsVersion.fromInsightsVersion('1.0.119.0');

/**
* Use the insights extension associated with the provided ARN. Make sure the ARN is associated
* with same region as your function
Expand All @@ -40,23 +59,35 @@ export abstract class LambdaInsightsVersion {
public static fromInsightVersionArn(arn: string): LambdaInsightsVersion {
class InsightsArn extends LambdaInsightsVersion {
public readonly layerVersionArn = arn;
public _bind(_scope: Construct, _function: IFunction): InsightsBindConfig {
return { 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
const versionExists = RegionInfo.regions.some(regionInfo => regionInfo.cloudwatchLambdaInsightsArn(insightsVersion));
if (!versionExists) {
throw new Error(`Insights version ${insightsVersion} does not exist.`);
}

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

public _bind(_scope: Construct, _function: IFunction): InsightsBindConfig {
const arch = _function.architecture?.name ?? Architecture.X86_64.name;
// Check if insights version is valid. This should only happen if one of the public static readonly versions are set incorrectly
// or if the version is not available for the Lambda Architecture
const versionExists = RegionInfo.regions.some(regionInfo => regionInfo.cloudwatchLambdaInsightsArn(insightsVersion, arch));
if (!versionExists) {
throw new Error(`Insights version ${insightsVersion} does not exist.`);
}
return {
arn: Lazy.uncachedString({
produce: (context) => getVersionArn(context, insightsVersion, arch),
}),
};
}
}
return new InsightsVersion();
}
Expand All @@ -65,6 +96,13 @@ export abstract class LambdaInsightsVersion {
* The arn of the Lambda Insights extension
*/
public readonly layerVersionArn: string = '';

/**
* Returns the arn of the Lambda Insights extension based on the
* Lambda architecture
* @internal
*/
public abstract _bind(_scope: Construct, _function: IFunction): InsightsBindConfig;
}

/**
Expand All @@ -73,14 +111,15 @@ export abstract class LambdaInsightsVersion {
*
* This function is run on CDK synthesis.
*/
function getVersionArn(context: IResolveContext, insightsVersion: string): string {
function getVersionArn(context: IResolveContext, insightsVersion: string, architecture?: string): string {

const scopeStack = Stack.of(context.scope);
const region = scopeStack.region;
const arch = architecture ?? Architecture.X86_64.name;

// 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);
const arn = RegionInfo.get(region).cloudwatchLambdaInsightsArn(insightsVersion, arch);
if (arn === undefined) {
throw new Error(`Insights version ${insightsVersion} is not supported in region ${region}`);
}
Expand Down Expand Up @@ -116,19 +155,33 @@ function getVersionArn(context: IResolveContext, insightsVersion: string): strin
* -- {'arn': 'arn3'},
* - us-east-2
* -- {'arn': 'arn4'}
* LambdaInsightsVersions101190arm64 // a separate mapping version 1.0.119.0 arm64
* - us-east-1
* -- {'arn': 'arn3'},
* - us-east-2
* -- {'arn': 'arn4'}
*/

const mapName = DEFAULT_MAPPING_PREFIX + insightsVersion.split('.').join('');
let mapName = DEFAULT_MAPPING_PREFIX + insightsVersion.split('.').join('');
// if the architecture is arm64 then append that to the end of the name
// this is so that we can have a separate mapping for x86 vs arm in scenarios
// where we have Lambda functions with both architectures in the same stack
if (arch === Architecture.ARM_64.name) {
mapName += arch;
}
const mapping: { [k1: string]: { [k2: string]: any } } = {};
const region2arns = RegionInfo.regionMap(FactName.cloudwatchLambdaInsightsVersion(insightsVersion));
const region2arns = RegionInfo.regionMap(FactName.cloudwatchLambdaInsightsVersion(insightsVersion, arch));
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 });
// need to call findInMap here if we are going to set lazy=true, otherwise
// we get the informLazyUse info message
const map = new CfnMapping(scopeStack, mapName, { mapping, lazy: true });
return map.findInMap(Aws.REGION, 'arn');
}
// The ARN will be looked up at deployment time from the mapping we created
return Fn.findInMap(mapName, Aws.REGION, 'arn');
}
}
11 changes: 8 additions & 3 deletions packages/@aws-cdk/aws-lambda/lib/lambda-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import { Fn, Lazy, RemovalPolicy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Alias, AliasOptions } from './alias';
import { Architecture } from './architecture';
import { EventInvokeConfigOptions } from './event-invoke-config';
import { Function } from './function';
import { IFunction, QualifiedFunctionBase } from './function-base';
Expand Down Expand Up @@ -127,11 +128,12 @@ export class Version extends QualifiedFunctionBase implements IVersion {
public readonly functionArn = versionArn;
public readonly grantPrincipal = lambda.grantPrincipal;
public readonly role = lambda.role;
public readonly architecture = lambda.architecture;

protected readonly qualifier = version;
protected readonly canCreatePermissions = this._isStackAccount();

public addAlias(name: string, opts: AliasOptions = { }): Alias {
public addAlias(name: string, opts: AliasOptions = {}): Alias {
return addAlias(this, this, name, opts);
}

Expand All @@ -153,11 +155,12 @@ export class Version extends QualifiedFunctionBase implements IVersion {
public readonly functionArn = `${attrs.lambda.functionArn}:${attrs.version}`;
public readonly grantPrincipal = attrs.lambda.grantPrincipal;
public readonly role = attrs.lambda.role;
public readonly architecture = attrs.lambda.architecture;

protected readonly qualifier = attrs.version;
protected readonly canCreatePermissions = this._isStackAccount();

public addAlias(name: string, opts: AliasOptions = { }): Alias {
public addAlias(name: string, opts: AliasOptions = {}): Alias {
return addAlias(this, this, name, opts);
}

Expand All @@ -175,6 +178,7 @@ export class Version extends QualifiedFunctionBase implements IVersion {
public readonly lambda: IFunction;
public readonly functionArn: string;
public readonly functionName: string;
public readonly architecture: Architecture;

protected readonly qualifier: string;
protected readonly canCreatePermissions = true;
Expand All @@ -183,6 +187,7 @@ export class Version extends QualifiedFunctionBase implements IVersion {
super(scope, id);

this.lambda = props.lambda;
this.architecture = props.lambda.architecture;

const version = new CfnVersion(this, 'Resource', {
codeSha256: props.codeSha256,
Expand Down Expand Up @@ -239,7 +244,7 @@ export class Version extends QualifiedFunctionBase implements IVersion {
* @param aliasName The name of the alias (e.g. "live")
* @param options Alias options
*/
public addAlias(aliasName: string, options: AliasOptions = { }): Alias {
public addAlias(aliasName: string, options: AliasOptions = {}): Alias {
return addAlias(this, this, aliasName, options);
}

Expand Down
Loading

0 comments on commit 02749b4

Please sign in to comment.