diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/aws-cdk-lambda-log-retention.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/aws-cdk-lambda-log-retention.template.json index 9eacf0b3d2ec3..3da7d872d969b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/aws-cdk-lambda-log-retention.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/aws-cdk-lambda-log-retention.template.json @@ -116,6 +116,34 @@ ], "Effect": "Allow", "Resource": "*" + }, + { + "Action": "logs:DeleteLogGroup", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/", + { + "Ref": "AutoDeleteLogGroupF74A7E0D" + }, + ":*" + ] + ] + } } ], "Version": "2012-10-17" @@ -296,6 +324,79 @@ }, "RetentionInDays": 365 } + }, + "AutoDeleteLogGroupServiceRoleF3400DE1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AutoDeleteLogGroupF74A7E0D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Role": { + "Fn::GetAtt": [ + "AutoDeleteLogGroupServiceRoleF3400DE1", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "AutoDeleteLogGroupServiceRoleF3400DE1" + ] + }, + "AutoDeleteLogGroupLogRetentionAC501FFB": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/lambda/", + { + "Ref": "AutoDeleteLogGroupF74A7E0D" + } + ] + ] + }, + "RemovalPolicy": "destroy" + } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/manifest.json index d48656a1d51b2..72ccefb2df9c3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/manifest.json @@ -105,6 +105,24 @@ "data": "OneYearLogRetentionBD83A067" } ], + "/aws-cdk-lambda-log-retention/AutoDeleteLogGroup/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "AutoDeleteLogGroupServiceRoleF3400DE1" + } + ], + "/aws-cdk-lambda-log-retention/AutoDeleteLogGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "AutoDeleteLogGroupF74A7E0D" + } + ], + "/aws-cdk-lambda-log-retention/AutoDeleteLogGroup/LogRetention/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "AutoDeleteLogGroupLogRetentionAC501FFB" + } + ], "/aws-cdk-lambda-log-retention/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/tree.json index de74bd7bfb09c..a023eaed19965 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.js.snapshot/tree.json @@ -225,6 +225,34 @@ ], "Effect": "Allow", "Resource": "*" + }, + { + "Action": "logs:DeleteLogGroup", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/", + { + "Ref": "AutoDeleteLogGroupF74A7E0D" + }, + ":*" + ] + ] + } } ], "Version": "2012-10-17" @@ -517,6 +545,115 @@ "fqn": "aws-cdk-lib.CfnRule", "version": "0.0.0" } + }, + "AutoDeleteLogGroup": { + "id": "AutoDeleteLogGroup", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/ServiceRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "zipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "role": { + "Fn::GetAtt": [ + "AutoDeleteLogGroupServiceRoleF3400DE1", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "nodejs14.x" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + }, + "LogRetention": { + "id": "LogRetention", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/LogRetention", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/LogRetention/Resource", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-logs.LogRetention", + "version": "0.0.0" + } + }, + "LogGroup": { + "id": "LogGroup", + "path": "aws-cdk-lambda-log-retention/AutoDeleteLogGroup/LogGroup", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.ts index 9b6247b5f19da..139a767576efc 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.log-retention.ts @@ -28,5 +28,12 @@ new lambda.Function(stack, 'OneYear', { logRetention: logs.RetentionDays.ONE_YEAR, }); +new lambda.Function(stack, 'AutoDeleteLogGroup', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + autoDeleteLogGroup: true, +}); + new IntegTest(app, 'LambdaLogRetentionInteg', { testCases: [stack] }); app.synth(); diff --git a/packages/aws-cdk-lib/aws-lambda/README.md b/packages/aws-cdk-lib/aws-lambda/README.md index 30b5839687637..224767a5320e9 100644 --- a/packages/aws-cdk-lib/aws-lambda/README.md +++ b/packages/aws-cdk-lib/aws-lambda/README.md @@ -944,6 +944,8 @@ It is possible to obtain the function's log group as a `logs.ILogGroup` by calli By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property allows you to customize the maximum number of retries and base backoff duration. +To automatically delete the associated log group for a Lambda function, you can use the `autoDeleteLogGroup` property. + *Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function.ts b/packages/aws-cdk-lib/aws-lambda/lib/function.ts index daef328fc8f50..d5b69a302695c 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function.ts @@ -27,7 +27,7 @@ import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as sns from '../../aws-sns'; import * as sqs from '../../aws-sqs'; -import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, IAspect, Lazy, Names, Size, Stack, Token } from '../../core'; +import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, IAspect, Lazy, Names, RemovalPolicy, Size, Stack, Token } from '../../core'; import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '../../cx-api'; /** @@ -296,6 +296,14 @@ export interface FunctionOptions extends EventInvokeConfigOptions { */ readonly events?: IEventSource[]; + /** + * Whether the associated CloudWatch log group should be automatically deleted + * when the function is removed from the stack or when the stack is deleted. + * + * @default false + */ + readonly autoDeleteLogGroup?: boolean; + /** * 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 @@ -866,12 +874,13 @@ export class Function extends FunctionBase { } // Log retention - if (props.logRetention) { + if (props.logRetention || props.autoDeleteLogGroup) { const logRetention = new logs.LogRetention(this, 'LogRetention', { logGroupName: `/aws/lambda/${this.functionName}`, - retention: props.logRetention, + retention: props.logRetention ?? logs.RetentionDays.INFINITE, role: props.logRetentionRole, - logRetentionRetryOptions: props.logRetentionRetryOptions as logs.LogRetentionRetryOptions, + logRetentionRetryOptions: props.logRetentionRetryOptions, + removalPolicy: props.autoDeleteLogGroup ? RemovalPolicy.DESTROY : undefined, }); this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logRetention.logGroupArn); } diff --git a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts index 83d25a6c13155..25db4badb85d6 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts @@ -1791,6 +1791,66 @@ describe('function', () => { }); }); + test('specify autoDeleteLogGroup', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS, + autoDeleteLogGroup: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { + LogGroupName: { + 'Fn::Join': [ + '', + [ + '/aws/lambda/', + { + Ref: 'MyLambdaCCE802FB', + }, + ], + ], + }, + RemovalPolicy: 'destroy', + }); + }); + + test('specify both log retention and autoDeleteLogGroup', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS, + logRetention: logs.RetentionDays.ONE_MONTH, + autoDeleteLogGroup: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { + LogGroupName: { + 'Fn::Join': [ + '', + [ + '/aws/lambda/', + { + Ref: 'MyLambdaCCE802FB', + }, + ], + ], + }, + RetentionInDays: 30, + RemovalPolicy: 'destroy', + }); + }); + test('imported lambda with imported security group and allowAllOutbound set to false', () => { // GIVEN const stack = new cdk.Stack();