Skip to content

Commit 63132ec

Browse files
jogoldElad Ben-Israel
authored and
Elad Ben-Israel
committed
feat(lambda): add support for log retention (#2067)
Adds a new property `logRetentionDays` on `Function` to control the log retention policy of the function logs in CloudWatch Logs. The implementation uses a Custom Resource to create the log group if it doesn't exist yet and to set the retention policy as discussed in #667. A retention policy of 1 day is set on the logs of the Lambda provider. The different retention days supported by CloudWatch Logs have been centralized in `@aws-cdk/aws-logs`. Some have been renamed to better match the console experience. Closes #667 BREAKING CHANGE: `cloudWatchLogsRetentionTimeDays` in `@aws-cdk/aws-cloudtrail` now uses a `logs.RetentionDays` instead of a `LogRetention`.
1 parent 7001f77 commit 63132ec

17 files changed

+1451
-35
lines changed

packages/@aws-cdk/aws-cloudtrail/lib/index.ts

+3-23
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export interface CloudTrailProps {
5858

5959
/**
6060
* How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false
61-
* @default LogRetention.OneYear
61+
* @default logs.RetentionDays.OneYear
6262
*/
63-
readonly cloudWatchLogsRetentionTimeDays?: LogRetention;
63+
readonly cloudWatchLogsRetentionTimeDays?: logs.RetentionDays;
6464

6565
/** The AWS Key Management Service (AWS KMS) key ID that you want to use to encrypt CloudTrail logs.
6666
* @default none
@@ -90,26 +90,6 @@ export enum ReadWriteType {
9090
All = "All"
9191
}
9292

93-
// TODO: This belongs in a CWL L2
94-
export enum LogRetention {
95-
OneDay = 1,
96-
ThreeDays = 3,
97-
FiveDays = 5,
98-
OneWeek = 7,
99-
TwoWeeks = 14,
100-
OneMonth = 30,
101-
TwoMonths = 60,
102-
ThreeMonths = 90,
103-
FourMonths = 120,
104-
FiveMonths = 150,
105-
HalfYear = 180,
106-
OneYear = 365,
107-
FourHundredDays = 400,
108-
EighteenMonths = 545,
109-
TwoYears = 731,
110-
FiveYears = 1827,
111-
TenYears = 3653
112-
}
11393
/**
11494
* Cloud trail allows you to log events that happen in your AWS account
11595
* For example:
@@ -145,7 +125,7 @@ export class CloudTrail extends cdk.Construct {
145125
let logsRole: iam.IRole | undefined;
146126
if (props.sendToCloudWatchLogs) {
147127
logGroup = new logs.CfnLogGroup(this, "LogGroup", {
148-
retentionInDays: props.cloudWatchLogsRetentionTimeDays || LogRetention.OneYear
128+
retentionInDays: props.cloudWatchLogsRetentionTimeDays || logs.RetentionDays.OneYear
149129
});
150130

151131
logsRole = new iam.Role(this, 'LogsRole', { assumedBy: new iam.ServicePrincipal(cloudTrailPrincipal) });

packages/@aws-cdk/aws-cloudtrail/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@
7272
"homepage": "https://github.com/awslabs/aws-cdk",
7373
"peerDependencies": {
7474
"@aws-cdk/aws-kms": "^0.26.0",
75+
"@aws-cdk/aws-logs": "^0.26.0",
7576
"@aws-cdk/cdk": "^0.26.0"
7677
},
7778
"engines": {
7879
"node": ">= 8.10.0"
7980
}
80-
}
81+
}

packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert';
2+
import { RetentionDays } from '@aws-cdk/aws-logs';
23
import { Stack } from '@aws-cdk/cdk';
34
import { Test } from 'nodeunit';
4-
import { CloudTrail, LogRetention, ReadWriteType } from '../lib';
5+
import { CloudTrail, ReadWriteType } from '../lib';
56

67
const ExpectedBucketPolicyProperties = {
78
PolicyDocument: {
@@ -105,7 +106,7 @@ export = {
105106
const stack = getTestStack();
106107
new CloudTrail(stack, 'MyAmazingCloudTrail', {
107108
sendToCloudWatchLogs: true,
108-
cloudWatchLogsRetentionTimeDays: LogRetention.OneWeek
109+
cloudWatchLogsRetentionTimeDays: RetentionDays.OneWeek
109110
});
110111

111112
expect(stack).to(haveResource("AWS::CloudTrail::Trail"));

packages/@aws-cdk/aws-lambda/lib/function.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
22
import ec2 = require('@aws-cdk/aws-ec2');
33
import iam = require('@aws-cdk/aws-iam');
4+
import logs = require('@aws-cdk/aws-logs');
45
import sqs = require('@aws-cdk/aws-sqs');
56
import cdk = require('@aws-cdk/cdk');
67
import { Code } from './code';
@@ -9,6 +10,7 @@ import { FunctionBase, FunctionImportProps, IFunction } from './function-base';
910
import { Version } from './lambda-version';
1011
import { CfnFunction } from './lambda.generated';
1112
import { ILayerVersion } from './layers';
13+
import { LogRetention } from './log-retention';
1214
import { Runtime } from './runtime';
1315

1416
/**
@@ -198,6 +200,15 @@ export interface FunctionProps {
198200
* You can also add event sources using `addEventSource`.
199201
*/
200202
readonly events?: IEventSource[];
203+
204+
/**
205+
* The number of days log events are kept in CloudWatch Logs. When updating
206+
* this property, unsetting it doesn't remove the log retention policy. To
207+
* remove the retention policy, set the value to `Infinity`.
208+
*
209+
* @default logs never expire
210+
*/
211+
readonly logRetentionDays?: logs.RetentionDays;
201212
}
202213

203214
/**
@@ -395,6 +406,14 @@ export class Function extends FunctionBase {
395406
for (const event of props.events || []) {
396407
this.addEventSource(event);
397408
}
409+
410+
// Log retention
411+
if (props.logRetentionDays) {
412+
new LogRetention(this, 'LogRetention', {
413+
logGroupName: `/aws/lambda/${this.functionName}`,
414+
retentionDays: props.logRetentionDays
415+
});
416+
}
398417
}
399418

400419
/**

packages/@aws-cdk/aws-lambda/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './lambda-version';
1010
export * from './singleton-lambda';
1111
export * from './event-source';
1212
export * from './event-source-mapping';
13+
export * from './log-retention';
1314

1415
// AWS::Lambda CloudFormation Resources:
1516
export * from './lambda.generated';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// tslint:disable:no-console
2+
import AWS = require('aws-sdk');
3+
4+
/**
5+
* Creates a log group and doesn't throw if it exists.
6+
*
7+
* @param logGroupName the name of the log group to create
8+
*/
9+
async function createLogGroupSafe(logGroupName: string) {
10+
try { // Try to create the log group
11+
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' });
12+
await cloudwatchlogs.createLogGroup({ logGroupName }).promise();
13+
} catch (e) {
14+
if (e.code !== 'ResourceAlreadyExistsException') {
15+
throw e;
16+
}
17+
}
18+
}
19+
20+
/**
21+
* Puts or deletes a retention policy on a log group.
22+
*
23+
* @param logGroupName the name of the log group to create
24+
* @param retentionInDays the number of days to retain the log events in the specified log group.
25+
*/
26+
async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) {
27+
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' });
28+
if (!retentionInDays) {
29+
await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise();
30+
} else {
31+
await cloudwatchlogs.putRetentionPolicy({ logGroupName, retentionInDays }).promise();
32+
}
33+
}
34+
35+
export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) {
36+
try {
37+
console.log(JSON.stringify(event));
38+
39+
// The target log group
40+
const logGroupName = event.ResourceProperties.LogGroupName;
41+
42+
if (event.RequestType === 'Create' || event.RequestType === 'Update') {
43+
// Act on the target log group
44+
await createLogGroupSafe(logGroupName);
45+
await setRetentionPolicy(logGroupName, parseInt(event.ResourceProperties.RetentionInDays, 10));
46+
47+
if (event.RequestType === 'Create') {
48+
// Set a retention policy of 1 day on the logs of this function. The log
49+
// group for this function should already exist at this stage because we
50+
// already logged the event but due to the async nature of Lambda logging
51+
// there could be a race condition. So we also try to create the log group
52+
// of this function first.
53+
await createLogGroupSafe(`/aws/lambda/${context.functionName}`);
54+
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1);
55+
}
56+
}
57+
58+
await respond('SUCCESS', 'OK', logGroupName);
59+
} catch (e) {
60+
console.log(e);
61+
62+
await respond('FAILED', e.message, event.ResourceProperties.LogGroupName);
63+
}
64+
65+
function respond(responseStatus: string, reason: string, physicalResourceId: string) {
66+
const responseBody = JSON.stringify({
67+
Status: responseStatus,
68+
Reason: reason,
69+
PhysicalResourceId: physicalResourceId,
70+
StackId: event.StackId,
71+
RequestId: event.RequestId,
72+
LogicalResourceId: event.LogicalResourceId,
73+
Data: {}
74+
});
75+
76+
console.log('Responding', responseBody);
77+
78+
const parsedUrl = require('url').parse(event.ResponseURL);
79+
const requestOptions = {
80+
hostname: parsedUrl.hostname,
81+
path: parsedUrl.path,
82+
method: 'PUT',
83+
headers: { 'content-type': '', 'content-length': responseBody.length }
84+
};
85+
86+
return new Promise((resolve, reject) => {
87+
try {
88+
const request = require('https').request(requestOptions, resolve);
89+
request.on('error', reject);
90+
request.write(responseBody);
91+
request.end();
92+
} catch (e) {
93+
reject(e);
94+
}
95+
});
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import iam = require('@aws-cdk/aws-iam');
2+
import logs = require('@aws-cdk/aws-logs');
3+
import cdk = require('@aws-cdk/cdk');
4+
import path = require('path');
5+
import { Code } from './code';
6+
import { Runtime } from './runtime';
7+
import { SingletonFunction } from './singleton-lambda';
8+
9+
/**
10+
* Construction properties for a LogRetention.
11+
*/
12+
export interface LogRetentionProps {
13+
/**
14+
* The log group name.
15+
*/
16+
readonly logGroupName: string;
17+
18+
/**
19+
* The number of days log events are kept in CloudWatch Logs.
20+
*/
21+
readonly retentionDays: logs.RetentionDays;
22+
}
23+
24+
/**
25+
* Creates a custom resource to control the retention policy of a CloudWatch Logs
26+
* log group. The log group is created if it doesn't already exist. The policy
27+
* is removed when `retentionDays` is `undefined` or equal to `Infinity`.
28+
*/
29+
export class LogRetention extends cdk.Construct {
30+
constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) {
31+
super(scope, id);
32+
33+
// Custom resource provider
34+
const provider = new SingletonFunction(this, 'Provider', {
35+
code: Code.asset(path.join(__dirname, 'log-retention-provider')),
36+
runtime: Runtime.NodeJS810,
37+
handler: 'index.handler',
38+
uuid: 'aae0aa3c-5b4d-4f87-b02d-85b201efdd8a',
39+
lambdaPurpose: 'LogRetention',
40+
});
41+
42+
if (provider.role && !provider.role.node.tryFindChild('DefaultPolicy')) { // Avoid duplicate statements
43+
provider.role.addToPolicy(
44+
new iam.PolicyStatement()
45+
.addActions('logs:PutRetentionPolicy', 'logs:DeleteRetentionPolicy')
46+
// We need '*' here because we will also put a retention policy on
47+
// the log group of the provider function. Referencing it's name
48+
// creates a CF circular dependency.
49+
.addAllResources()
50+
);
51+
}
52+
53+
// Need to use a CfnResource here to prevent lerna dependency cycles
54+
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
55+
new cdk.CfnResource(this, 'Resource', {
56+
type: 'Custom::LogRetention',
57+
properties: {
58+
ServiceToken: provider.functionArn,
59+
LogGroupName: props.logGroupName,
60+
RetentionInDays: props.retentionDays === Infinity ? undefined : props.retentionDays
61+
}
62+
});
63+
}
64+
}

0 commit comments

Comments
 (0)