Repository: serverless/serverless (JavaScript) — Feature

Issue:
Feature Proposal: Create Log Groups for lambda functions as part of Cloudformation
# This is a Feature Proposal
## Description

When creating the functions in the cloudformation template it would be ideal if the relevant log groups were created as well. 

For feature proposals:
- What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us.

This would allow for setting a retention policy on the logs to help with costs. It also would allow for hooking up of a subscription to handle alerting and processing of logs. This is hard at the moment due to the fact that the log groups are only created after the lambda has executed once. It will also mean the log groups can be cleaned up as part of `sls remove` where they are being left there currently clogging up the interface.
- If there is additional config how would it look

```
{
    "Type" : "AWS::Logs::LogGroup",
    "Properties" : {
        "LogGroupName" : "/aws/lambda/${FunctionName}"
    }
}
```

It would be simple to add it around https://github.com/serverless/serverless/blob/master/lib/plugins/aws/deploy/compile/functions/index.js#L170 
## Additional Data
- **_Serverless Framework Version you're using**_: Master
- **_Operating System**_: Win
- **_Stack Trace**_:
- **_Provider Error messages**_:



Discussion:

Resolving patch:
diff --git a/docs/02-providers/aws/02-iam.md b/docs/02-providers/aws/02-iam.md
index 5d671cac56d..918694e12f5 100644
--- a/docs/02-providers/aws/02-iam.md
+++ b/docs/02-providers/aws/02-iam.md
@@ -41,3 +41,15 @@ provider:
   name: aws
   iamRoleARN: arn:aws:iam::YourAccountNumber:role/YourIamRole
 ```
+
+# Explicitly creating LogGroups Resources
+By default, the framework does not create LogGroups for your Lambdas. However this behavior will be deprecated soon and we'll be adding CloudFormation LogGroups resources as part of the stack. This makes it easy to clean up your log groups in the case you remove your service, and make the lambda IAM permissions much more specific and secure.
+
+To opt in for this feature now to avoid breaking changes later, add the following to your provider config in `serverless.yml`:
+
+```yml
+provider:
+  cfLogs: true
+```
+If you get a CloudFormation error saying that log group already exists, you have to remove it first from AWS console, then deploy, otherwise for new services this should work out of the box.
+
diff --git a/lib/plugins/aws/deploy/lib/configureStack.js b/lib/plugins/aws/deploy/lib/configureStack.js
index 92edb8b5383..48d024c0f56 100644
--- a/lib/plugins/aws/deploy/lib/configureStack.js
+++ b/lib/plugins/aws/deploy/lib/configureStack.js
@@ -14,6 +14,7 @@ module.exports = {
         'lib',
         'core-cloudformation-template.json')
     );
+
     const bucketName = this.serverless.service.provider.deploymentBucket;
 
     if (bucketName) {
diff --git a/lib/plugins/aws/deploy/lib/iam-policy-lambda-execution-template.json b/lib/plugins/aws/deploy/lib/iam-policy-lambda-execution-template.json
index 49c56626797..e59725e91d8 100644
--- a/lib/plugins/aws/deploy/lib/iam-policy-lambda-execution-template.json
+++ b/lib/plugins/aws/deploy/lib/iam-policy-lambda-execution-template.json
@@ -10,10 +10,16 @@
             "Effect": "Allow",
             "Action": [
               "logs:CreateLogGroup",
-              "logs:CreateLogStream",
+              "logs:CreateLogStream"
+            ],
+            "Resource": []
+          },
+          {
+            "Effect": "Allow",
+            "Action": [
               "logs:PutLogEvents"
             ],
-            "Resource": ""
+            "Resource": []
           }
         ]
       },
diff --git a/lib/plugins/aws/deploy/lib/mergeIamTemplates.js b/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
index e14067a318d..97a8e19a35b 100644
--- a/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
@@ -36,16 +36,76 @@ module.exports = {
         .Properties
         .PolicyName = `${this.options.stage}-${this.serverless.service.service}-lambda`;
 
-      iamPolicyLambdaExecutionTemplate
-        .IamPolicyLambdaExecution
-        .Properties
-        .PolicyDocument
-        .Statement[0]
-        .Resource = `arn:aws:logs:${this.options.region}:*:*`;
-
       _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
         iamPolicyLambdaExecutionTemplate);
 
+      if (!this.serverless.service.provider.cfLogs) {
+        this.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .IamPolicyLambdaExecution
+          .Properties
+          .PolicyDocument
+          .Statement[0]
+          .Resource = `arn:aws:logs:${this.options.region}:*:*`;
+
+        this.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .IamPolicyLambdaExecution
+          .Properties
+          .PolicyDocument
+          .Statement[1]
+          .Resource = `arn:aws:logs:${this.options.region}:*:*`;
+
+        const warningMessage = [
+          'Deprecation Notice: Starting with the next update, ',
+          'we will drop support for Lambda to implicitly create LogGroups. ',
+          'Please remove your log groups and set "provider.cfLogs: true", ' +
+          'for CloudFormation to explicitly create them for you.',
+        ].join('');
+        this.serverless.cli.log(warningMessage);
+      } else {
+        this.serverless.service.getAllFunctions().forEach((functionName) => {
+          const functionObject = this.serverless.service.getFunction(functionName);
+          const normalizedFunctionName = functionName[0].toUpperCase() + functionName.substr(1);
+          const logGroupTemplate = `
+          {
+            "${normalizedFunctionName}LogGroup": {
+              "Type" : "AWS::Logs::LogGroup",
+              "Properties" : {
+                "LogGroupName" : "/aws/lambda/${functionObject.name}"
+              }
+            }
+          }
+        `;
+          const newLogGroup = JSON.parse(logGroupTemplate);
+          _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+            newLogGroup);
+
+          this.serverless.service.provider.compiledCloudFormationTemplate
+            .Resources
+            .IamPolicyLambdaExecution
+            .Properties
+            .PolicyDocument
+            .Statement[0]
+            .Resource
+            .push({ 'Fn::GetAtt': [`${normalizedFunctionName}LogGroup`, 'Arn'] });
+
+          this.serverless.service.provider.compiledCloudFormationTemplate
+            .Resources
+            .IamPolicyLambdaExecution
+            .Properties
+            .PolicyDocument
+            .Statement[1]
+            .Resource
+            .push({
+              'Fn::Join': [
+                ':',
+                [
+                  { 'Fn::GetAtt': [`${normalizedFunctionName}LogGroup`, 'Arn'] },
+                  '*',
+                ],
+              ],
+            });
+        });
+      }
 
       // add custom iam role statements
       if (this.serverless.service.provider.iamRoleStatements &&


Test patch:
diff --git a/lib/plugins/aws/deploy/compile/functions/tests/index.js b/lib/plugins/aws/deploy/compile/functions/tests/index.js
index 2f5b4c921ca..36f98972955 100644
--- a/lib/plugins/aws/deploy/compile/functions/tests/index.js
+++ b/lib/plugins/aws/deploy/compile/functions/tests/index.js
@@ -13,11 +13,11 @@ describe('AwsCompileFunctions', () => {
   const compiledFunctionName = 'TestLambdaFunction';
 
   beforeEach(() => {
-    serverless = new Serverless();
     const options = {
       stage: 'dev',
       region: 'us-east-1',
     };
+    serverless = new Serverless(options);
     serverless.setProvider('aws', new AwsProvider(serverless));
     awsCompileFunctions = new AwsCompileFunctions(serverless, options);
     awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = {
diff --git a/lib/plugins/aws/deploy/tests/configureStack.js b/lib/plugins/aws/deploy/tests/configureStack.js
index 2d5efc66873..be7b5e2fcbe 100644
--- a/lib/plugins/aws/deploy/tests/configureStack.js
+++ b/lib/plugins/aws/deploy/tests/configureStack.js
@@ -12,6 +12,7 @@ const configureStack = require('../lib/configureStack');
 describe('#configureStack', () => {
   let serverless;
   const awsPlugin = {};
+  const functionName = 'test';
 
   beforeEach(() => {
     serverless = new Serverless();
@@ -21,7 +22,23 @@ describe('#configureStack', () => {
       stage: 'dev',
       region: 'us-east-1',
     };
+
     Object.assign(awsPlugin, configureStack, validate);
+
+    awsPlugin.serverless.cli = new serverless.classes.CLI();
+
+    awsPlugin.serverless.service.provider.compiledCloudFormationTemplate = {
+      Resources: {},
+      Outputs: {},
+    };
+    awsPlugin.serverless.service.service = 'new-service';
+    awsPlugin.serverless.service.functions = {
+      [functionName]: {
+        name: 'test',
+        artifact: 'test.zip',
+        handler: 'handler.hello',
+      },
+    };
   });
 
   it('should validate the region for the given S3 bucket', () => {
diff --git a/lib/plugins/aws/deploy/tests/mergeIamTemplates.js b/lib/plugins/aws/deploy/tests/mergeIamTemplates.js
index 821114dc6b7..f35ef1fe2e3 100644
--- a/lib/plugins/aws/deploy/tests/mergeIamTemplates.js
+++ b/lib/plugins/aws/deploy/tests/mergeIamTemplates.js
@@ -9,6 +9,7 @@ const AwsDeploy = require('../');
 describe('#mergeIamTemplates()', () => {
   let awsDeploy;
   let serverless;
+  const functionName = 'test';
 
   beforeEach(() => {
     serverless = new Serverless();
@@ -21,6 +22,14 @@ describe('#mergeIamTemplates()', () => {
     awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = {
       Resources: {},
     };
+    awsDeploy.serverless.service.service = 'new-service';
+    awsDeploy.serverless.service.functions = {
+      [functionName]: {
+        name: 'test',
+        artifact: 'test.zip',
+        handler: 'handler.hello',
+      },
+    };
   });
 
 
@@ -42,8 +51,8 @@ describe('#mergeIamTemplates()', () => {
       });
   });
 
-  it('should merge IamPolicyLambdaExecution template into the CloudFormation template', () =>
-    awsDeploy.mergeIamTemplates()
+  it('should merge IamPolicyLambdaExecution template into the CloudFormation template',
+    () => awsDeploy.mergeIamTemplates()
       .then(() => {
         // we check for the type here because a deep equality check will error out due to
         // the updates which are made after the merge (they are tested in a separate test)
@@ -53,8 +62,8 @@ describe('#mergeIamTemplates()', () => {
       })
   );
 
-  it('should update the necessary variables for the IamPolicyLambdaExecution', () =>
-    awsDeploy.mergeIamTemplates()
+  it('should update the necessary variables for the IamPolicyLambdaExecution',
+    () => awsDeploy.mergeIamTemplates()
       .then(() => {
         expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
           .Resources
@@ -68,15 +77,6 @@ describe('#mergeIamTemplates()', () => {
             awsDeploy.serverless.service.service
             }-lambda`
         );
-
-        expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
-          .Resources
-          .IamPolicyLambdaExecution
-          .Properties
-          .PolicyDocument
-          .Statement[0]
-          .Resource
-        ).to.equal(`arn:aws:logs:${awsDeploy.options.region}:*:*`);
       })
   );
 
@@ -96,7 +96,7 @@ describe('#mergeIamTemplates()', () => {
     return awsDeploy.mergeIamTemplates()
       .then(() => {
         expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
-          .Resources.IamPolicyLambdaExecution.Properties.PolicyDocument.Statement[1]
+          .Resources.IamPolicyLambdaExecution.Properties.PolicyDocument.Statement[2]
         ).to.deep.equal(awsDeploy.serverless.service.provider.iamRoleStatements[0]);
       });
   });
@@ -121,4 +121,132 @@ describe('#mergeIamTemplates()', () => {
             .Resources.IamRoleLambdaExecution
         ).to.not.exist);
   });
+
+  it('should add a CloudWatch LogGroup resource', () => {
+    awsDeploy.serverless.service.provider.cfLogs = true;
+    const normalizedName = `${functionName[0].toUpperCase()}${functionName.substr(1)}LogGroup`;
+    return awsDeploy.mergeIamTemplates().then(() => {
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources[normalizedName]
+      ).to.deep.equal(
+        {
+          Type: 'AWS::Logs::LogGroup',
+          Properties: {
+            LogGroupName: `/aws/lambda/${functionName}`,
+          },
+        }
+      );
+    });
+  });
+
+  it('should update IamPolicyLambdaExecution with a logging resource for the function', () => {
+    awsDeploy.serverless.service.provider.cfLogs = true;
+    awsDeploy.serverless.service.functions = {
+      func0: {
+        handler: 'func.function.handler',
+        name: 'func0',
+      },
+      func1: {
+        handler: 'func.function.handler',
+        name: 'func1',
+      },
+    };
+    const f = awsDeploy.serverless.service.functions;
+    const normalizedNames = [
+      `${f.func0.name[0].toUpperCase()}${f.func0.name.substr(1)}LogGroup`,
+      `${f.func1.name[0].toUpperCase()}${f.func1.name.substr(1)}LogGroup`,
+    ];
+    return awsDeploy.mergeIamTemplates().then(() => {
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources[normalizedNames[0]]
+      ).to.deep.equal(
+        {
+          Type: 'AWS::Logs::LogGroup',
+          Properties: {
+            LogGroupName: `/aws/lambda/${f.func0.name}`,
+          },
+        }
+      );
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources[normalizedNames[1]]
+      ).to.deep.equal(
+        {
+          Type: 'AWS::Logs::LogGroup',
+          Properties: {
+            LogGroupName: `/aws/lambda/${f.func1.name}`,
+          },
+        }
+      );
+    });
+  });
+
+  it('should update IamPolicyLambdaExecution with a logging resource for the function', () => {
+    awsDeploy.serverless.service.provider.cfLogs = true;
+    const normalizedName = `${functionName[0].toUpperCase()}${functionName.substr(1)}LogGroup`;
+    return awsDeploy.mergeIamTemplates().then(() => {
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources
+        .IamPolicyLambdaExecution
+        .Properties
+        .PolicyDocument
+        .Statement[0]
+        .Resource
+      ).to.deep.equal([{ 'Fn::GetAtt': [normalizedName, 'Arn'] }]);
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources
+        .IamPolicyLambdaExecution
+        .Properties
+        .PolicyDocument
+        .Statement[1]
+        .Resource
+      ).to.deep.equal([{ 'Fn::Join': [':', [{ 'Fn::GetAtt': [normalizedName, 'Arn'] }, '*']] }]);
+    });
+  });
+
+  it('should update IamPolicyLambdaExecution with each function\'s logging resources', () => {
+    awsDeploy.serverless.service.provider.cfLogs = true;
+    awsDeploy.serverless.service.functions = {
+      func0: {
+        handler: 'func.function.handler',
+        name: 'func0',
+      },
+      func1: {
+        handler: 'func.function.handler',
+        name: 'func1',
+      },
+    };
+    const f = awsDeploy.serverless.service.functions; // avoid 100 char lines below
+    const normalizedNames = [
+      `${f.func0.name[0].toUpperCase()}${f.func0.name.substr(1)}LogGroup`,
+      `${f.func1.name[0].toUpperCase()}${f.func1.name.substr(1)}LogGroup`,
+    ];
+    return awsDeploy.mergeIamTemplates().then(() => {
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources
+        .IamPolicyLambdaExecution
+        .Properties
+        .PolicyDocument
+        .Statement[0]
+        .Resource
+      ).to.deep.equal(
+        [
+          { 'Fn::GetAtt': [normalizedNames[0], 'Arn'] },
+          { 'Fn::GetAtt': [normalizedNames[1], 'Arn'] },
+        ]
+      );
+      expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources
+        .IamPolicyLambdaExecution
+        .Properties
+        .PolicyDocument
+        .Statement[1]
+        .Resource
+      ).to.deep.equal(
+        [
+          { 'Fn::Join': [':', [{ 'Fn::GetAtt': [normalizedNames[0], 'Arn'] }, '*']] },
+          { 'Fn::Join': [':', [{ 'Fn::GetAtt': [normalizedNames[1], 'Arn'] }, '*']] },
+        ]
+      );
+    });
+  });
 });