From cc773813beab26a4b8caf1bfcf12be0fcc39b589 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 21 Mar 2019 09:54:52 +0100 Subject: [PATCH] feat(lambda): add support for log retention 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`. --- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 26 +- packages/@aws-cdk/aws-cloudtrail/package.json | 3 +- .../aws-cloudtrail/test/test.cloudtrail.ts | 5 +- packages/@aws-cdk/aws-lambda/lib/function.ts | 49 +++ .../aws-lambda/lib/log-retention/index.ts | 100 +++++ .../@aws-cdk/aws-lambda/package-lock.json | 122 ++++++ packages/@aws-cdk/aws-lambda/package.json | 4 +- .../test/integ.log-retention.expected.json | 359 ++++++++++++++++++ .../aws-lambda/test/integ.log-retention.ts | 30 ++ .../@aws-cdk/aws-lambda/test/test.lambda.ts | 53 +++ packages/@aws-cdk/aws-logs/lib/log-group.ts | 94 ++++- .../aws-logs/test/example.retention.lit.ts | 4 +- .../@aws-cdk/aws-logs/test/test.loggroup.ts | 4 +- 13 files changed, 820 insertions(+), 33 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda/lib/log-retention/index.ts create mode 100644 packages/@aws-cdk/aws-lambda/package-lock.json create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index e6f0c2cd52744..3b436de3eddba 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -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 @@ -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: @@ -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) }); diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 7704ad357fb4e..831b5c347abc0 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 1197811a5f2bf..04a3dff6c899d 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -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: { @@ -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")); diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 657219c575cfc..1e4d30d681a50 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -1,8 +1,10 @@ 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'; @@ -10,6 +12,7 @@ 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) @@ -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; } /** @@ -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 */ @@ -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 + } + }); + } } /** diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention/index.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention/index.ts new file mode 100644 index 0000000000000..e0bba9278980c --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention/index.ts @@ -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); + } + }); + } +} diff --git a/packages/@aws-cdk/aws-lambda/package-lock.json b/packages/@aws-cdk/aws-lambda/package-lock.json new file mode 100644 index 0000000000000..78c72f9c689f1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/package-lock.json @@ -0,0 +1,122 @@ +{ + "name": "@aws-cdk/aws-lambda", + "version": "0.26.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/aws-lambda": { + "version": "8.10.23", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.23.tgz", + "integrity": "sha512-erfexxfuc1+T7b4OswooKwpIjpdgEOVz6ZrDDWSR+3v7Kjhs4EVowfUkF9KuLKhpcjz+VVHQ/pWIl7zSVbKbFQ==", + "dev": true + }, + "aws-sdk": { + "version": "2.425.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.425.0.tgz", + "integrity": "sha512-SM2qZJPlZUKVzSSqNuCvONOhJ2kcFvU+hAwutjQeje2VKpSAbUbFCFWl6cki2FjiyGZYEPfl0Q+3ANJO8gx9BA==", + "dev": true, + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "dev": true + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "dev": true + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + } + } +} diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index e804d9f979383..df24204b2eadb 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -59,6 +59,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.26.0", + "@types/aws-lambda": "^8.10.23", + "aws-sdk": "^2.425.0", "cdk-build-tools": "^0.26.0", "cdk-integ-tools": "^0.26.0", "cfn2ts": "^0.26.0", @@ -97,4 +99,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json new file mode 100644 index 0000000000000..a894418c333ff --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -0,0 +1,359 @@ +{ + "Resources": { + "OneWeekServiceRole05A6F9F8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneWeekFE56F6A4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneWeekServiceRole05A6F9F8", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneWeekServiceRole05A6F9F8" + ] + }, + "OneWeekLogRetentionCustomResourceFDF55BE4": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "FunctionName": { + "Ref": "OneWeekFE56F6A4" + }, + "RetentionInDays": 7 + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + ] + }, + "OneMonthServiceRoleFBD1064F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneMonth64E966BF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneMonthServiceRoleFBD1064F", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneMonthServiceRoleFBD1064F" + ] + }, + "OneMonthLogRetentionCustomResource6DB54B5C": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "FunctionName": { + "Ref": "OneMonth64E966BF" + }, + "RetentionInDays": 30 + } + }, + "OneYearServiceRole24D47762": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneYearA82EBDA9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneYearServiceRole24D47762", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneYearServiceRole24D47762" + ] + }, + "OneYearLogRetentionCustomResource23BBD73D": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "FunctionName": { + "Ref": "OneYearA82EBDA9" + }, + "RetentionInDays": 365 + } + } + }, + "Parameters": { + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts new file mode 100644 index 0000000000000..08475cc79d5b8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts @@ -0,0 +1,30 @@ +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import lambda = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-lambda-log-retention'); + +new lambda.Function(stack, 'OneWeek', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneWeek +}); + +new lambda.Function(stack, 'OneMonth', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneMonth +}); + +new lambda.Function(stack, 'OneYear', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneYear +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index b24c2b0d81844..d1813d6da8488 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,6 +1,7 @@ import { countResources, expect, haveResource, MatchStyle, ResourcePart } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); 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 { Test } from 'nodeunit'; @@ -1296,6 +1297,58 @@ export = { test.equal(rt.supportsInlineCode, false); test.done(); + }, + + 'specify log retention'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS, + logRetentionDays: logs.RetentionDays.OneMonth + }); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + })); + + expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyLambdaCCE802FB" + }, + "RetentionInDays": 30 + })); + + test.done(); + } }; diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index c0df6719e0cc6..d38c6839c1f51 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -189,6 +189,96 @@ export abstract class LogGroupBase extends cdk.Construct implements ILogGroup { } } +/** + * How long, in days, the log contents will be retained. + */ +export enum RetentionDays { + /** + * 1 day + */ + OneDay = 1, + + /** + * 3 days + */ + ThreeDays = 3, + + /** + * 5 days + */ + FiveDays = 5, + + /** + * 1 week + */ + OneWeek = 7, + + /** + * 2 weeks + */ + TwoWeeks = 14, + + /** + * 1 month + */ + OneMonth = 30, + + /** + * 2 months + */ + TwoMonths = 60, + + /** + * 3 months + */ + ThreeMonths = 90, + + /** + * 4 months + */ + FourMonths = 120, + + /** + * 5 months + */ + FiveMonths = 150, + + /** + * 6 months + */ + SixMonths = 180, + + /** + * 1 year + */ + OneYear = 365, + + /** + * 13 months + */ + ThirteenMonths = 400, + + /** + * 18 months + */ + EighteenMonths = 545, + + /** + * 2 years + */ + TwoYears = 731, + + /** + * 5 years + */ + FiveYears = 1827, + + /** + * 10 years + */ + TenYears = 3653 +} + /** * Properties for a LogGroup */ @@ -207,7 +297,7 @@ export interface LogGroupProps { * * @default 731 days (2 years) */ - retentionDays?: number; + retentionDays?: RetentionDays; /** * Retain the log group if the stack or containing construct ceases to exist @@ -247,7 +337,7 @@ export class LogGroup extends LogGroupBase { super(scope, id); let retentionInDays = props.retentionDays; - if (retentionInDays === undefined) { retentionInDays = 731; } + if (retentionInDays === undefined) { retentionInDays = RetentionDays.TwoYears; } if (retentionInDays === Infinity) { retentionInDays = undefined; } if (retentionInDays !== undefined && retentionInDays <= 0) { diff --git a/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts b/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts index 72e7a180c6267..ed0fd084e18d4 100644 --- a/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts +++ b/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts @@ -1,5 +1,5 @@ import { Stack } from '@aws-cdk/cdk'; -import { LogGroup } from '../lib'; +import { LogGroup, RetentionDays } from '../lib'; const stack = new Stack(); @@ -7,7 +7,7 @@ function shortLogGroup() { /// !show // Configure log group for short retention const logGroup = new LogGroup(stack, 'LogGroup', { - retentionDays: 7 + retentionDays: RetentionDays.OneWeek }); /// !hide return logGroup; diff --git a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts index f23d3b16f8002..418197a35839f 100644 --- a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts +++ b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts @@ -2,7 +2,7 @@ import { expect, haveResource, matchTemplate } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { LogGroup } from '../lib'; +import { LogGroup, RetentionDays } from '../lib'; export = { 'fixed retention'(test: Test) { @@ -11,7 +11,7 @@ export = { // WHEN new LogGroup(stack, 'LogGroup', { - retentionDays: 7 + retentionDays: RetentionDays.OneWeek }); // THEN