Skip to content

Commit

Permalink
feat(lambda): add support for log retention
Browse files Browse the repository at this point in the history
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 aws#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 aws#667

BREAKING CHANGE: `cloudWatchLogsRetentionTimeDays` in `@aws-cdk/aws-cloudtrail`
now uses a `logs.RetentionDays` instead of a `LogRetention`.
  • Loading branch information
jogold committed Mar 21, 2019
1 parent bf79c82 commit cc77381
Show file tree
Hide file tree
Showing 13 changed files with 820 additions and 33 deletions.
26 changes: 3 additions & 23 deletions packages/@aws-cdk/aws-cloudtrail/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export interface CloudTrailProps {

/**
* How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false
* @default LogRetention.OneYear
* @default logs.RetentionDays.OneYear
*/
cloudWatchLogsRetentionTimeDays?: LogRetention;
cloudWatchLogsRetentionTimeDays?: logs.RetentionDays;

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

// TODO: This belongs in a CWL L2
export enum LogRetention {
OneDay = 1,
ThreeDays = 3,
FiveDays = 5,
OneWeek = 7,
TwoWeeks = 14,
OneMonth = 30,
TwoMonths = 60,
ThreeMonths = 90,
FourMonths = 120,
FiveMonths = 150,
HalfYear = 180,
OneYear = 365,
FourHundredDays = 400,
EighteenMonths = 545,
TwoYears = 731,
FiveYears = 1827,
TenYears = 3653
}
/**
* Cloud trail allows you to log events that happen in your AWS account
* For example:
Expand Down Expand Up @@ -145,7 +125,7 @@ export class CloudTrail extends cdk.Construct {
let logsRole: iam.IRole | undefined;
if (props.sendToCloudWatchLogs) {
logGroup = new logs.CfnLogGroup(this, "LogGroup", {
retentionInDays: props.cloudWatchLogsRetentionTimeDays || LogRetention.OneYear
retentionInDays: props.cloudWatchLogsRetentionTimeDays || logs.RetentionDays.OneYear
});

logsRole = new iam.Role(this, 'LogsRole', { assumedBy: new iam.ServicePrincipal(cloudTrailPrincipal) });
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-cloudtrail/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@
"homepage": "https://github.com/awslabs/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-kms": "^0.26.0",
"@aws-cdk/aws-logs": "^0.26.0",
"@aws-cdk/cdk": "^0.26.0"
},
"engines": {
"node": ">= 8.10.0"
}
}
}
5 changes: 3 additions & 2 deletions packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect, haveResource, not } from '@aws-cdk/assert';
import { RetentionDays } from '@aws-cdk/aws-logs';
import { Stack } from '@aws-cdk/cdk';
import { Test } from 'nodeunit';
import { CloudTrail, LogRetention, ReadWriteType } from '../lib';
import { CloudTrail, ReadWriteType } from '../lib';

const ExpectedBucketPolicyProperties = {
PolicyDocument: {
Expand Down Expand Up @@ -105,7 +106,7 @@ export = {
const stack = getTestStack();
new CloudTrail(stack, 'MyAmazingCloudTrail', {
sendToCloudWatchLogs: true,
cloudWatchLogsRetentionTimeDays: LogRetention.OneWeek
cloudWatchLogsRetentionTimeDays: RetentionDays.OneWeek
});

expect(stack).to(haveResource("AWS::CloudTrail::Trail"));
Expand Down
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import ec2 = require('@aws-cdk/aws-ec2');
import iam = require('@aws-cdk/aws-iam');
import logs = require('@aws-cdk/aws-logs');
import sqs = require('@aws-cdk/aws-sqs');
import cdk = require('@aws-cdk/cdk');
import path = require('path');
import { Code } from './code';
import { IEventSource } from './event-source';
import { FunctionBase, FunctionImportProps, IFunction } from './function-base';
import { Version } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
import { Runtime } from './runtime';
import { SingletonFunction } from './singleton-lambda';

/**
* X-Ray Tracing Modes (https://docs.aws.amazon.com/lambda/latest/dg/API_TracingConfig.html)
Expand Down Expand Up @@ -198,6 +201,15 @@ export interface FunctionProps {
* You can also add event sources using `addEventSource`.
*/
events?: IEventSource[];

/**
* The number of days log events are kept in CloudWatch Logs. When updating
* this property, unsetting it doesn't remove the log retention policy. To
* remove the retention policy, set the value to `Infinity`.
*
* @default logs never expire
*/
logRetentionDays?: logs.RetentionDays;
}

/**
Expand Down Expand Up @@ -297,6 +309,8 @@ export class Function extends FunctionBase {
return this.metricAll('UnreservedConcurrentExecutions', { statistic: 'max', ...props });
}

private static logRetentionRolePolicy: boolean = false;

/**
* Name of this function
*/
Expand Down Expand Up @@ -395,6 +409,41 @@ export class Function extends FunctionBase {
for (const event of props.events || []) {
this.addEventSource(event);
}

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

if (!Function.logRetentionRolePolicy) { // Avoid duplicate statements
provider.addToRolePolicy(
new iam.PolicyStatement()
.addActions('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.
.addAllResources()
);
Function.logRetentionRolePolicy = true;
}

// Need to use a CfnResource here to prevent lerna dependency cycles
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
new cdk.CfnResource(this, 'LogRetentionCustomResource', {
type: 'AWS::CloudFormation::CustomResource',
properties: {
ServiceToken: provider.functionArn,
FunctionName: this.functionName,
RetentionInDays: props.logRetentionDays === Infinity ? undefined : props.logRetentionDays
}
});
}
}

/**
Expand Down
100 changes: 100 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/log-retention/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// tslint:disable:no-console
import AWS = require('aws-sdk');

const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' });

/**
* Creates a log group and doesn't throw if it exists.
*
* @param logGroupName the name of the log group to create
*/
/* istanbul ignore next */
async function createLogGroupSafe(logGroupName: string) {
try { // Try to create the log group
await cloudwatchlogs.createLogGroup({ logGroupName }).promise();
} catch (e) {
if (e.code !== 'ResourceAlreadyExistsException') {
throw e;
}
}
}

/**
* Puts or deletes a retention policy on a log group.
*
* @param logGroupName the name of the log group to create
* @param retentionInDays the number of days to retain the log events in the specified log group.
*/
/* istanbul ignore next */
async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) {
if (!retentionInDays) {
await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise();
} else {
await cloudwatchlogs.putRetentionPolicy({ logGroupName, retentionInDays }).promise();
}
}

/* istanbul ignore next */
export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) {
try {
console.log(JSON.stringify(event));

// The target log group
const logGroupName = `/aws/lambda/${event.ResourceProperties.FunctionName}`;

if (event.RequestType === 'Create' || event.RequestType === 'Update') {
// Act on the target log group
await createLogGroupSafe(logGroupName);
await setRetentionPolicy(logGroupName, event.ResourceProperties.RetentionInDays);

if (event.RequestType === 'Create') {
// Set a retention policy of 1 day on the logs of this function. The log
// group for this function should already exist at this stage because we
// already logged the event but due to the async nature of Lambda logging
// there could be a race condition. So we also try to create the log group
// of this function first.
await createLogGroupSafe(`/aws/lambda/${context.functionName}`);
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1);
}
}

await respond('SUCCESS', 'OK', logGroupName);
} catch (e) {
console.log(e);

await respond('FAILED', e.message, context.logStreamName);
}

function respond(responseStatus: string, reason: string, physicalResourceId: string) {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: reason,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: {}
});

console.log('Responding', responseBody);

const parsedUrl = require('url').parse(event.ResponseURL);
const requestOptions = {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
method: 'PUT',
headers: { 'content-type': '', 'content-length': responseBody.length }
};

return new Promise((resolve, reject) => {
try {
const request = require('https').request(requestOptions, resolve);
request.on('error', reject);
request.write(responseBody);
request.end();
} catch (e) {
reject(e);
}
});
}
}
122 changes: 122 additions & 0 deletions packages/@aws-cdk/aws-lambda/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit cc77381

Please sign in to comment.