Skip to content

Commit

Permalink
feat(logs): specify log group's region for LogRetention (#9804)
Browse files Browse the repository at this point in the history
Global AWS services follow the convention of auto-creating CloudWatch Logs in `us-east-1`, which
makes it challenging to control the log group retention when the stack is created in other region.

This change adds support for specifying the optional region for LogRetention to be able to set log retention on CloudWatch logs created by global AWS Services in `us-east-1`

Closes #9703


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Stacy-D committed Sep 2, 2020
1 parent 25cfc18 commit 0ccbc5d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 8 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-logs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ retention period (including infinite retention).

[retention example](test/example.retention.lit.ts)

### LogRetention

The `LogRetention` construct is a way to control the retention period of log groups that are created outside of the CDK. The construct is usually
used on log groups that are auto created by AWS services, such as [AWS
lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html).

This is implemented using a [CloudFormation custom
resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html)
which pre-creates the log group if it doesn't exist, and sets the specified log retention period (never expire, by default).

By default, the log group will be created in the same region as the stack. The `logGroupRegion` property can be used to configure
log groups in other regions. This is typically useful when controlling retention for log groups auto-created by global services that
publish their log group to a specific region, such as AWS Chatbot creating a log group in `us-east-1`.

### Subscriptions and Destinations

Log events matching a particular filter can be sent to either a Lambda function
Expand Down
22 changes: 14 additions & 8 deletions packages/@aws-cdk/aws-logs/lib/log-retention-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ interface SdkRetryOptions {
* Creates a log group and doesn't throw if it exists.
*
* @param logGroupName the name of the log group to create.
* @param region to create the log group in
* @param options CloudWatch API SDK options.
*/
async function createLogGroupSafe(logGroupName: string, options?: SdkRetryOptions) {
async function createLogGroupSafe(logGroupName: string, region?: string, options?: SdkRetryOptions) {
try { // Try to create the log group
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options });
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options });
await cloudwatchlogs.createLogGroup({ logGroupName }).promise();
} catch (e) {
if (e.code !== 'ResourceAlreadyExistsException') {
Expand All @@ -31,11 +32,12 @@ async function createLogGroupSafe(logGroupName: string, options?: SdkRetryOption
* Puts or deletes a retention policy on a log group.
*
* @param logGroupName the name of the log group to create
* @param region the region of the log group
* @param options CloudWatch API SDK options.
* @param retentionInDays the number of days to retain the log events in the specified log group.
*/
async function setRetentionPolicy(logGroupName: string, options?: SdkRetryOptions, retentionInDays?: number) {
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options });
async function setRetentionPolicy(logGroupName: string, region?: string, options?: SdkRetryOptions, retentionInDays?: number) {
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options });
if (!retentionInDays) {
await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise();
} else {
Expand All @@ -50,13 +52,16 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
// The target log group
const logGroupName = event.ResourceProperties.LogGroupName;

// The region of the target log group
const logGroupRegion = event.ResourceProperties.LogGroupRegion;

// Parse to AWS SDK retry options
const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry);

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

if (event.RequestType === 'Create') {
// Set a retention policy of 1 day on the logs of this function. The log
Expand All @@ -68,8 +73,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
// same time. This can sometime result in an OperationAbortedException. To
// avoid this and because this operation is not critical we catch all errors.
try {
await createLogGroupSafe(`/aws/lambda/${context.functionName}`, retryOptions);
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, retryOptions, 1);
const region = process.env.AWS_REGION;
await createLogGroupSafe(`/aws/lambda/${context.functionName}`, region, retryOptions);
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, region, retryOptions, 1);
} catch (e) {
console.log(e);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-logs/lib/log-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export interface LogRetentionProps {
*/
readonly logGroupName: string;

/**
* The region where the log group should be created
* @default - same region as the stack
*/
readonly logGroupRegion?: string;

/**
* The number of days log events are kept in CloudWatch Logs.
*/
Expand Down Expand Up @@ -55,6 +61,8 @@ export interface LogRetentionRetryOptions {
* 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`.
* Log group can be created in the region that is different from stack region by
* specifying `logGroupRegion`
*/
export class LogRetention extends cdk.Construct {

Expand All @@ -77,6 +85,7 @@ export class LogRetention extends cdk.Construct {
properties: {
ServiceToken: provider.functionArn,
LogGroupName: props.logGroupName,
LogGroupRegion: props.logGroupRegion,
SdkRetry: retryOptions ? {
maxRetries: retryOptions.maxRetries,
base: retryOptions.base?.toMilliseconds(),
Expand All @@ -89,6 +98,7 @@ export class LogRetention extends cdk.Construct {
// 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({
region: props.logGroupRegion,
service: 'logs',
resource: 'log-group',
resourceName: `${logGroupName}:*`,
Expand Down
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export = {
sinon.assert.calledWith(AWSSDK.CloudWatchLogs as any, {
apiVersion: '2014-03-28',
maxRetries: 5,
region: undefined,
retryOptions: {
base: 300,
},
Expand All @@ -333,4 +334,34 @@ export = {
test.done();
},

async 'custom log retention region'(test: Test) {
AWS.mock('CloudWatchLogs', 'createLogGroup', sinon.fake.resolves({}));
AWS.mock('CloudWatchLogs', 'putRetentionPolicy', sinon.fake.resolves({}));
AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', sinon.fake.resolves({}));

const event = {
...eventCommon,
RequestType: 'Create',
ResourceProperties: {
ServiceToken: 'token',
RetentionInDays: '30',
LogGroupName: 'group',
LogGroupRegion: 'us-east-1',
},
};

const request = createRequest('SUCCESS');

await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context);

sinon.assert.calledWith(AWSSDK.CloudWatchLogs as any, {
apiVersion: '2014-03-28',
region: 'us-east-1',
});

test.equal(request.isDone(), true);

test.done();
},

};
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-logs/test/test.log-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ export = {
test.done();
},

'with LogGroupRegion specified'(test: Test) {
const stack = new cdk.Stack();
new LogRetention(stack, 'MyLambda', {
logGroupName: 'group',
logGroupRegion: 'us-east-1',
retention: RetentionDays.INFINITE,
});

expect(stack).to(haveResource('Custom::LogRetention', {
LogGroupRegion: 'us-east-1',
}));

test.done();
},

'log group ARN is well formed and conforms'(test: Test) {
const stack = new cdk.Stack();
const group = new LogRetention(stack, 'MyLambda', {
Expand All @@ -122,4 +137,20 @@ export = {
test.ok(logGroupArn.endsWith(':*'), 'log group ARN is not as expected');
test.done();
},

'log group ARN is well formed and conforms when region is specified'(test: Test) {
const stack = new cdk.Stack();
const group = new LogRetention(stack, 'MyLambda', {
logGroupName: 'group',
logGroupRegion: 'us-west-2',
retention: RetentionDays.ONE_MONTH,
});

const logGroupArn = group.logGroupArn;
test.ok(logGroupArn.indexOf('us-west-2') > -1, 'region of log group ARN is not as expected');
test.ok(logGroupArn.indexOf('logs') > -1, 'log group ARN is not as expected');
test.ok(logGroupArn.indexOf('log-group') > -1, 'log group ARN is not as expected');
test.ok(logGroupArn.endsWith(':*'), 'log group ARN is not as expected');
test.done();
},
};

0 comments on commit 0ccbc5d

Please sign in to comment.