Skip to content

Commit e17a49a

Browse files
feat(lambda): configurable retries for log retention custom resource (#8258)
This prevents throttling issues on stacks with a lot of Lambdas. fixes #8257 Implemented configurable `maxRetries` and `base` properties as part of the LogRetentionRetryOptions. The AWS SDK also supports specifying a customBackoff function. I skipped that as it's hard to implement in the current setup (impossible to provide a callback function in the event JSON). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a56a4d9 commit e17a49a

File tree

7 files changed

+135
-29
lines changed

7 files changed

+135
-29
lines changed

packages/@aws-cdk/aws-lambda/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ The `logRetention` property can be used to set a different expiration period.
266266
It is possible to obtain the function's log group as a `logs.ILogGroup` by calling the `logGroup` property of the
267267
`Function` construct.
268268

269+
By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property
270+
allows you to customize the maximum number of retries and base backoff duration.
271+
269272
*Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom
270273
resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added
271274
to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { calculateFunctionHash, trimFromStart } from './function-hash';
1212
import { Version, VersionOptions } from './lambda-version';
1313
import { CfnFunction } from './lambda.generated';
1414
import { ILayerVersion } from './layers';
15-
import { LogRetention } from './log-retention';
15+
import { LogRetention, LogRetentionRetryOptions } from './log-retention';
1616
import { Runtime } from './runtime';
1717

1818
/**
@@ -232,6 +232,14 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
232232
*/
233233
readonly logRetentionRole?: iam.IRole;
234234

235+
/**
236+
* When log retention is specified, a custom resource attempts to create the CloudWatch log group.
237+
* These options control the retry policy when interacting with CloudWatch APIs.
238+
*
239+
* @default - Default AWS SDK retry options.
240+
*/
241+
readonly logRetentionRetryOptions?: LogRetentionRetryOptions;
242+
235243
/**
236244
* Options for the `lambda.Version` resource automatically created by the
237245
* `fn.currentVersion` method.
@@ -544,6 +552,7 @@ export class Function extends FunctionBase {
544552
logGroupName: `/aws/lambda/${this.functionName}`,
545553
retention: props.logRetention,
546554
role: props.logRetentionRole,
555+
logRetentionRetryOptions: props.logRetentionRetryOptions,
547556
});
548557
this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logretention.logGroupArn);
549558
}

packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
// eslint-disable-next-line import/no-extraneous-dependencies
44
import * as AWS from 'aws-sdk';
5+
// eslint-disable-next-line import/no-extraneous-dependencies
6+
import { RetryDelayOptions } from 'aws-sdk/lib/config';
7+
8+
interface SdkRetryOptions {
9+
maxRetries?: number;
10+
retryOptions?: RetryDelayOptions;
11+
}
512

613
/**
714
* Creates a log group and doesn't throw if it exists.
815
*
9-
* @param logGroupName the name of the log group to create
16+
* @param logGroupName the name of the log group to create.
17+
* @param options CloudWatch API SDK options.
1018
*/
11-
async function createLogGroupSafe(logGroupName: string) {
19+
async function createLogGroupSafe(logGroupName: string, options?: SdkRetryOptions) {
1220
try { // Try to create the log group
13-
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' });
21+
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options });
1422
await cloudwatchlogs.createLogGroup({ logGroupName }).promise();
1523
} catch (e) {
1624
if (e.code !== 'ResourceAlreadyExistsException') {
@@ -23,10 +31,11 @@ async function createLogGroupSafe(logGroupName: string) {
2331
* Puts or deletes a retention policy on a log group.
2432
*
2533
* @param logGroupName the name of the log group to create
34+
* @param options CloudWatch API SDK options.
2635
* @param retentionInDays the number of days to retain the log events in the specified log group.
2736
*/
28-
async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) {
29-
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' });
37+
async function setRetentionPolicy(logGroupName: string, options?: SdkRetryOptions, retentionInDays?: number) {
38+
const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options });
3039
if (!retentionInDays) {
3140
await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise();
3241
} else {
@@ -41,10 +50,13 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
4150
// The target log group
4251
const logGroupName = event.ResourceProperties.LogGroupName;
4352

53+
// Parse to AWS SDK retry options
54+
const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry);
55+
4456
if (event.RequestType === 'Create' || event.RequestType === 'Update') {
4557
// Act on the target log group
46-
await createLogGroupSafe(logGroupName);
47-
await setRetentionPolicy(logGroupName, parseInt(event.ResourceProperties.RetentionInDays, 10));
58+
await createLogGroupSafe(logGroupName, retryOptions);
59+
await setRetentionPolicy(logGroupName, retryOptions, parseInt(event.ResourceProperties.RetentionInDays, 10));
4860

4961
if (event.RequestType === 'Create') {
5062
// Set a retention policy of 1 day on the logs of this function. The log
@@ -56,8 +68,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
5668
// same time. This can sometime result in an OperationAbortedException. To
5769
// avoid this and because this operation is not critical we catch all errors.
5870
try {
59-
await createLogGroupSafe(`/aws/lambda/${context.functionName}`);
60-
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1);
71+
await createLogGroupSafe(`/aws/lambda/${context.functionName}`, retryOptions);
72+
await setRetentionPolicy(`/aws/lambda/${context.functionName}`, retryOptions, 1);
6173
} catch (e) {
6274
console.log(e);
6375
}
@@ -108,4 +120,19 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
108120
}
109121
});
110122
}
123+
124+
function parseRetryOptions(rawOptions: any): SdkRetryOptions {
125+
const retryOptions: SdkRetryOptions = {};
126+
if (rawOptions) {
127+
if (rawOptions.maxRetries) {
128+
retryOptions.maxRetries = parseInt(rawOptions.maxRetries, 10);
129+
}
130+
if (rawOptions.base) {
131+
retryOptions.retryOptions = {
132+
base: parseInt(rawOptions.base, 10),
133+
};
134+
}
135+
}
136+
return retryOptions;
137+
}
111138
}

packages/@aws-cdk/aws-lambda/lib/log-retention.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ export interface LogRetentionProps {
2626
* @default - A new role is created
2727
*/
2828
readonly role?: iam.IRole;
29+
30+
/**
31+
* Retry options for all AWS API calls.
32+
*
33+
* @default - AWS SDK default retry options
34+
*/
35+
readonly logRetentionRetryOptions?: LogRetentionRetryOptions;
36+
}
37+
38+
/**
39+
* Retry options for all AWS API calls.
40+
*/
41+
export interface LogRetentionRetryOptions {
42+
/**
43+
* The maximum amount of retries.
44+
*
45+
* @default 3 (AWS SDK default)
46+
*/
47+
readonly maxRetries?: number;
48+
/**
49+
* The base duration to use in the exponential backoff for operation retries.
50+
*
51+
* @default Duration.millis(100) (AWS SDK default)
52+
*/
53+
readonly base?: cdk.Duration;
2954
}
3055

3156
/**
@@ -64,11 +89,16 @@ export class LogRetention extends cdk.Construct {
6489

6590
// Need to use a CfnResource here to prevent lerna dependency cycles
6691
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
92+
const retryOptions = props.logRetentionRetryOptions;
6793
const resource = new cdk.CfnResource(this, 'Resource', {
6894
type: 'Custom::LogRetention',
6995
properties: {
7096
ServiceToken: provider.functionArn,
7197
LogGroupName: props.logGroupName,
98+
SdkRetry: retryOptions ? {
99+
maxRetries: retryOptions.maxRetries,
100+
base: retryOptions.base?.toMilliseconds(),
101+
} : undefined,
72102
RetentionInDays: props.retention === logs.RetentionDays.INFINITE ? undefined : props.retention,
73103
},
74104
});

packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
"Properties": {
134134
"Code": {
135135
"S3Bucket": {
136-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE"
136+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049"
137137
},
138138
"S3Key": {
139139
"Fn::Join": [
@@ -146,7 +146,7 @@
146146
"Fn::Split": [
147147
"||",
148148
{
149-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583"
149+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1"
150150
}
151151
]
152152
}
@@ -159,7 +159,7 @@
159159
"Fn::Split": [
160160
"||",
161161
{
162-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583"
162+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1"
163163
}
164164
]
165165
}
@@ -331,17 +331,17 @@
331331
}
332332
},
333333
"Parameters": {
334-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": {
334+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": {
335335
"Type": "String",
336-
"Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
336+
"Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
337337
},
338-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": {
338+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": {
339339
"Type": "String",
340-
"Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
340+
"Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
341341
},
342-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": {
342+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": {
343343
"Type": "String",
344-
"Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
344+
"Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
345345
}
346346
}
347347
}

packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,41 @@ export = {
296296

297297
test.done();
298298
},
299+
300+
async 'custom log retention retry options'(test: Test) {
301+
AWS.mock('CloudWatchLogs', 'createLogGroup', sinon.fake.resolves({}));
302+
AWS.mock('CloudWatchLogs', 'putRetentionPolicy', sinon.fake.resolves({}));
303+
AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', sinon.fake.resolves({}));
304+
305+
const event = {
306+
...eventCommon,
307+
RequestType: 'Create',
308+
ResourceProperties: {
309+
ServiceToken: 'token',
310+
RetentionInDays: '30',
311+
LogGroupName: 'group',
312+
SdkRetry: {
313+
maxRetries: '5',
314+
base: '300',
315+
},
316+
},
317+
};
318+
319+
const request = createRequest('SUCCESS');
320+
321+
await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context);
322+
323+
sinon.assert.calledWith(AWSSDK.CloudWatchLogs as any, {
324+
apiVersion: '2014-03-28',
325+
maxRetries: 5,
326+
retryOptions: {
327+
base: 300,
328+
},
329+
});
330+
331+
test.equal(request.isDone(), true);
332+
333+
test.done();
334+
},
335+
299336
};

packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@
967967
"Properties": {
968968
"Code": {
969969
"S3Bucket": {
970-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE"
970+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049"
971971
},
972972
"S3Key": {
973973
"Fn::Join": [
@@ -980,7 +980,7 @@
980980
"Fn::Split": [
981981
"||",
982982
{
983-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583"
983+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1"
984984
}
985985
]
986986
}
@@ -993,7 +993,7 @@
993993
"Fn::Split": [
994994
"||",
995995
{
996-
"Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583"
996+
"Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1"
997997
}
998998
]
999999
}
@@ -1108,17 +1108,17 @@
11081108
}
11091109
},
11101110
"Parameters": {
1111-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": {
1111+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": {
11121112
"Type": "String",
1113-
"Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
1113+
"Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
11141114
},
1115-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": {
1115+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": {
11161116
"Type": "String",
1117-
"Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
1117+
"Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
11181118
},
1119-
"AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": {
1119+
"AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": {
11201120
"Type": "String",
1121-
"Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
1121+
"Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\""
11221122
}
11231123
}
1124-
}
1124+
}

0 commit comments

Comments
 (0)