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

refactor(lambda): move log retention custom resource to logs (#9671) #9808

Merged
merged 15 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { calculateFunctionHash, trimFromStart } from './function-hash';
import { Version, VersionOptions } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
import { LogRetention, LogRetentionRetryOptions } from './log-retention';
import { LogRetentionRetryOptions } from './log-retention';
import { Runtime } from './runtime';

/**
Expand Down Expand Up @@ -633,13 +633,13 @@ export class Function extends FunctionBase {

// Log retention
if (props.logRetention) {
const logretention = new LogRetention(this, 'LogRetention', {
const logRetention = new logs.LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: props.logRetention,
role: props.logRetentionRole,
logRetentionRetryOptions: props.logRetentionRetryOptions,
logRetentionRetryOptions: props.logRetentionRetryOptions as logs.LogRetentionRetryOptions,
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logretention.logGroupArn);
this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logRetention.logGroupArn);
}

props.code.bindToResource(resource);
Expand Down Expand Up @@ -759,11 +759,11 @@ export class Function extends FunctionBase {
*/
public get logGroup(): logs.ILogGroup {
if (!this._logGroup) {
const logretention = new LogRetention(this, 'LogRetention', {
const logRetention = new logs.LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: logs.RetentionDays.INFINITE,
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logretention.logGroupArn);
this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logRetention.logGroupArn);
}
return this._logGroup;
}
Expand Down
109 changes: 12 additions & 97 deletions packages/@aws-cdk/aws-lambda/lib/log-retention.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,31 @@
import * as path from 'path';
import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';
import * as cdk from '@aws-cdk/core';
import { Code } from './code';
import { Runtime } from './runtime';
import { SingletonFunction } from './singleton-lambda';

/**
* Construction properties for a LogRetention.
* Retry options for all AWS API calls.
*
* @deprecated use `LogRetentionRetryOptions` from '@aws-cdk/aws-logs' instead
*/
export interface LogRetentionProps {
/**
* The log group name.
*/
readonly logGroupName: string;

/**
* The number of days log events are kept in CloudWatch Logs.
*/
readonly retention: logs.RetentionDays;

/**
* The IAM role for the Lambda function associated with the custom resource.
*
* @default - A new role is created
*/
readonly role?: iam.IRole;

/**
* Retry options for all AWS API calls.
*
* @default - AWS SDK default retry options
*/
readonly logRetentionRetryOptions?: LogRetentionRetryOptions;
export interface LogRetentionRetryOptions extends logs.LogRetentionRetryOptions {
humanzz marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Retry options for all AWS API calls.
* Construction properties for a LogRetention.
*
* @deprecated use `LogRetentionProps` from '@aws-cdk/aws-logs' instead
*/
export interface LogRetentionRetryOptions {
/**
* The maximum amount of retries.
*
* @default 3 (AWS SDK default)
*/
readonly maxRetries?: number;
/**
* The base duration to use in the exponential backoff for operation retries.
*
* @default Duration.millis(100) (AWS SDK default)
*/
readonly base?: cdk.Duration;
export interface LogRetentionProps extends logs.LogRetentionProps {
}

/**
* Creates a custom resource to control the retention policy of a CloudWatch Logs
* log group. The log group is created if it doesn't already exist. The policy
* is removed when `retentionDays` is `undefined` or equal to `Infinity`.
*
* @deprecated use `LogRetention` from '@aws-cdk/aws-logs' instead
*/
export class LogRetention extends cdk.Construct {

/**
* The ARN of the LogGroup.
*/
public readonly logGroupArn: string;

export class LogRetention extends logs.LogRetention {
constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) {
super(scope, id);

// Custom resource provider
const provider = new SingletonFunction(this, 'Provider', {
code: Code.fromAsset(path.join(__dirname, 'log-retention-provider')),
runtime: Runtime.NODEJS_10_X,
handler: 'index.handler',
uuid: 'aae0aa3c-5b4d-4f87-b02d-85b201efdd8a',
lambdaPurpose: 'LogRetention',
role: props.role,
});

// Duplicate statements will be deduplicated by `PolicyDocument`
provider.addToRolePolicy(new iam.PolicyStatement({
actions: ['logs:PutRetentionPolicy', 'logs:DeleteRetentionPolicy'],
// We need '*' here because we will also put a retention policy on
// the log group of the provider function. Referencing it's name
// creates a CF circular dependency.
resources: ['*'],
}));

// Need to use a CfnResource here to prevent lerna dependency cycles
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
const retryOptions = props.logRetentionRetryOptions;
const resource = new cdk.CfnResource(this, 'Resource', {
type: 'Custom::LogRetention',
properties: {
ServiceToken: provider.functionArn,
LogGroupName: props.logGroupName,
SdkRetry: retryOptions ? {
maxRetries: retryOptions.maxRetries,
base: retryOptions.base?.toMilliseconds(),
} : undefined,
RetentionInDays: props.retention === logs.RetentionDays.INFINITE ? undefined : props.retention,
},
});

const logGroupName = resource.getAtt('LogGroupName').toString();
// Append ':*' at the end of the ARN to match with how CloudFormation does this for LogGroup ARNs
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values
this.logGroupArn = cdk.Stack.of(this).formatArn({
service: 'logs',
resource: 'log-group',
resourceName: `${logGroupName}:*`,
sep: ':',
});
super(scope, id, { ...props });
}
}
7 changes: 1 addition & 6 deletions packages/@aws-cdk/aws-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,12 @@
"@types/aws-lambda": "^8.10.61",
"@types/lodash": "^4.14.160",
"@types/nodeunit": "^0.0.31",
"@types/sinon": "^9.0.5",
"aws-sdk": "^2.739.0",
"aws-sdk-mock": "^5.1.0",
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"cfn2ts": "0.0.0",
"lodash": "^4.17.20",
"nock": "^13.0.4",
"nodeunit": "^0.11.3",
"pkglint": "0.0.0",
"sinon": "^9.0.3"
"pkglint": "0.0.0"
},
"dependencies": {
"@aws-cdk/aws-applicationautoscaling": "0.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-logs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './log-stream';
export * from './metric-filter';
export * from './pattern';
export * from './subscription-filter';
export * from './log-retention';

// AWS::Logs CloudFormation Resources:
export * from './logs.generated';
166 changes: 166 additions & 0 deletions packages/@aws-cdk/aws-logs/lib/log-retention.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as path from 'path';
import * as iam from '@aws-cdk/aws-iam';
import * as s3_assets from '@aws-cdk/aws-s3-assets';
import * as cdk from '@aws-cdk/core';
import { RetentionDays } from './log-group';

/**
* Construction properties for a LogRetention.
*/
export interface LogRetentionProps {
/**
* The log group name.
*/
readonly logGroupName: string;

/**
* The number of days log events are kept in CloudWatch Logs.
*/
readonly retention: RetentionDays;

/**
* The IAM role for the Lambda function associated with the custom resource.
*
* @default - A new role is created
*/
readonly role?: iam.IRole;

/**
* Retry options for all AWS API calls.
*
* @default - AWS SDK default retry options
*/
readonly logRetentionRetryOptions?: LogRetentionRetryOptions;
}

/**
* Retry options for all AWS API calls.
*/
export interface LogRetentionRetryOptions {
/**
* The maximum amount of retries.
*
* @default 3 (AWS SDK default)
*/
readonly maxRetries?: number;
/**
* The base duration to use in the exponential backoff for operation retries.
*
* @default Duration.millis(100) (AWS SDK default)
*/
readonly base?: cdk.Duration;
}

/**
* Creates a custom resource to control the retention policy of a CloudWatch Logs
* log group. The log group is created if it doesn't already exist. The policy
* is removed when `retentionDays` is `undefined` or equal to `Infinity`.
*/
export class LogRetention extends cdk.Construct {

/**
* The ARN of the LogGroup.
*/
public readonly logGroupArn: string;

constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) {
super(scope, id);

// Custom resource provider
const provider = this.ensureSingletonLogRetentionFunction(props);

// Need to use a CfnResource here to prevent lerna dependency cycles
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
const retryOptions = props.logRetentionRetryOptions;
const resource = new cdk.CfnResource(this, 'Resource', {
type: 'Custom::LogRetention',
properties: {
ServiceToken: provider.functionArn,
LogGroupName: props.logGroupName,
SdkRetry: retryOptions ? {
maxRetries: retryOptions.maxRetries,
base: retryOptions.base?.toMilliseconds(),
} : undefined,
RetentionInDays: props.retention === RetentionDays.INFINITE ? undefined : props.retention,
},
});

const logGroupName = resource.getAtt('LogGroupName').toString();
// Append ':*' at the end of the ARN to match with how CloudFormation does this for LogGroup ARNs
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values
this.logGroupArn = cdk.Stack.of(this).formatArn({
service: 'logs',
resource: 'log-group',
resourceName: `${logGroupName}:*`,
sep: ':',
});
}

/**
* Helper method to ensure that only one instance of LogRetentionFunction resources are in the stack mimicking the
* behaviour of @aws-cdk/aws-lambda's SingletonFunction to prevent circular dependencies
*/
private ensureSingletonLogRetentionFunction(props: LogRetentionProps) {
const functionLogicalId = 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a';
const existing = cdk.Stack.of(this).node.tryFindChild(functionLogicalId);
if (existing) {
return existing as LogRetentionFunction;
}
return new LogRetentionFunction(cdk.Stack.of(this), functionLogicalId, props);
}
}

/**
* Private provider Lambda function to support the log retention custom resource.
*/
class LogRetentionFunction extends cdk.Construct {
public readonly functionArn: cdk.Reference;

constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) {
super(scope, id);

// Code
const asset = new s3_assets.Asset(this, 'Code', {
path: path.join(__dirname, 'log-retention-provider'),
});

// Role
const role = props.role || new iam.Role(this, 'ServiceRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
});
// Duplicate statements will be deduplicated by `PolicyDocument`
role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['logs:PutRetentionPolicy', 'logs:DeleteRetentionPolicy'],
// We need '*' here because we will also put a retention policy on
// the log group of the provider function. Referencing its name
// creates a CF circular dependency.
resources: ['*'],
}));

// Lambda function
const resource = new cdk.CfnResource(this, 'Resource', {
type: 'AWS::Lambda::Function',
properties: {
Handler: 'index.handler',
Runtime: 'nodejs10.x',
Code: {
S3Bucket: asset.s3BucketName,
S3Key: asset.s3ObjectKey,
},
Role: role.roleArn,
},
});
this.functionArn = resource.getAtt('Arn');

// Function dependencies
role.node.children.forEach((child) => {
if (cdk.CfnResource.isCfnResource(child)) {
resource.addDependsOn(child);
}
if (cdk.Construct.isConstruct(child) && child.node.defaultChild && cdk.CfnResource.isCfnResource(child.node.defaultChild)) {
resource.addDependsOn(child.node.defaultChild);
}
});
}
}
8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-logs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,28 @@
"devDependencies": {
"@aws-cdk/assert": "0.0.0",
"@types/nodeunit": "^0.0.31",
"aws-sdk": "^2.715.0",
"aws-sdk-mock": "^5.1.0",
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"cfn2ts": "0.0.0",
"nock": "^13.0.2",
"nodeunit": "^0.11.3",
"pkglint": "0.0.0"
"pkglint": "0.0.0",
"sinon": "^9.0.2"
},
"dependencies": {
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-s3-assets": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.0.4"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-s3-assets": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.0.4"
},
Expand Down
Loading