Repository: serverless/serverless (JavaScript)
Date: 2016-12-05 13:29:59+00:00
Task type: Feature

Issue:
Improve "Invoke Local"
# Problem
We've added `invoke local` with the goal of emulating Lambda as best as it can, so that developers can use it to speed up their workflow.

We tried to get an MVP of local support in ASAP, and succeeded.  However, it missed a few critical pieces:  

**1 - `invoke local` behaves differently than `invoke`**

**2 - `invoke local` does not include the environment variables available in AWS Lambda**

**3 - `invoke local` does not support our other runtimes**

# Solution

### 1

This is already being fixed by @horike37 in PR #2620.

### 2

We should create a function that collects the ENV vars before the local invocation happens.  Here are the missing ENV vars.  We have enough information to find/create most of their values locally.

`LAMBDA_TASK_ROOT`
`LAMBDA_RUNTIME_DIR`
`AWS_REGION`
`AWS_DEFAULT_REGION`
`AWS_LAMBDA_LOG_GROUP_NAME`
`AWS_LAMBDA_LOG_STREAM_NAME`
`AWS_LAMBDA_FUNCTION_NAME`
`AWS_LAMBDA_FUNCTION_MEMORY_SIZE`
`AWS_LAMBDA_FUNCTION_VERSION`
`AWS_ACCESS_KEY_ID`
`AWS_SECRET_ACCESS_KEY`
`AWS_SESSION_TOKEN`

### 3

To easily support other languages, we should use a child process whenever we do a local invocation.  We can pass the above environment variables into that child process as an option, since that's easily supported by Node.js.

Keep in mind, the function can be long-running.  Using a callback with the child_process via something like `exec` may not be recommended, since log statements should appear whenever they are logged to `stdout`.  `on('data')` [may be preferred](https://nodejs.org/api/child_process.html#child_process_child_process)

**Note:** A lot of this functionality was already written in V.0 and is a good reference: https://github.com/serverless/serverless/blob/8239e9ca030bfbfa3e8c3f3a9d90aa6af6aec83b/lib/RuntimePython27.js


Issue discussion:
Question, in the next release will be possible to invoke other runtimes? like java/scala?
I'm trying **invoke local** right now: I noticed it ignores the specified AWS profile in _serverless.yaml_ (`profile: bbb-dev`) and uses the default one `test-cli` (which, in my case, doesn't have access to a SNS topic, hence the invocation fails).

`
message: 'User: arn:aws:iam::*******:user/test-cli is not authorized to perform: SNS:Publish on resource: arn:aws:sns:us-east-1:*******:bbb-test',
`

Is that a known issue or am I doing something wrong?
_NB: Just to be clear: if I invoke it remotely, everything works._

Solution patch:
diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js
index 6247e899d6f..e56105023f1 100644
--- a/lib/plugins/aws/invokeLocal/index.js
+++ b/lib/plugins/aws/invokeLocal/index.js
@@ -52,10 +52,31 @@ class AwsInvokeLocal {
   }
 
   loadEnvVars() {
+    const lambdaName = this.options.functionObj.name;
+    const memorySize = Number(this.options.functionObj.memorySize)
+      || Number(this.serverless.service.provider.memorySize)
+      || 1024;
+
+    const lambdaDefaultEnvVars = {
+      PATH: '/usr/local/lib64/node-v4.3.x/bin:/usr/local/bin:/usr/bin/:/bin',
+      LANG: 'en_US.UTF-8',
+      LD_LIBRARY_PATH: '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len
+      LAMBDA_TASK_ROOT: '/var/task',
+      LAMBDA_RUNTIME_DIR: '/var/runtime',
+      AWS_REGION: this.options.region,
+      AWS_DEFAULT_REGION: this.options.region,
+      AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName),
+      AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad',
+      AWS_LAMBDA_FUNCTION_NAME: lambdaName,
+      AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
+      AWS_LAMBDA_FUNCTION_VERSION: '$LATEST',
+      NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
+    };
+
     const providerEnvVars = this.serverless.service.provider.environment || {};
     const functionEnvVars = this.options.functionObj.environment || {};
 
-    _.merge(process.env, providerEnvVars, functionEnvVars);
+    _.merge(process.env, lambdaDefaultEnvVars, providerEnvVars, functionEnvVars);
 
     return BbPromise.resolve();
   }


Test patch:
diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js
index a407cc6c9f0..cfa68c0e26a 100644
--- a/lib/plugins/aws/invokeLocal/index.test.js
+++ b/lib/plugins/aws/invokeLocal/index.test.js
@@ -185,7 +185,9 @@ describe('AwsInvokeLocal', () => {
       };
 
       awsInvokeLocal.options = {
+        region: 'us-east-1',
         functionObj: {
+          name: 'serviceName-dev-hello',
           environment: {
             functionVar: 'functionValue',
           },
@@ -205,6 +207,26 @@ describe('AwsInvokeLocal', () => {
       })
     );
 
+    it('it should load default lambda env vars', () => awsInvokeLocal
+      .loadEnvVars().then(() => {
+        expect(process.env.PATH)
+          .to.equal('/usr/local/lib64/node-v4.3.x/bin:/usr/local/bin:/usr/bin/:/bin');
+        expect(process.env.LANG).to.equal('en_US.UTF-8');
+        expect(process.env.LD_LIBRARY_PATH)
+          .to.equal('/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib'); // eslint-disable-line max-len
+        expect(process.env.LAMBDA_TASK_ROOT).to.equal('/var/task');
+        expect(process.env.LAMBDA_RUNTIME_DIR).to.equal('/var/runtime');
+        expect(process.env.AWS_REGION).to.equal('us-east-1');
+        expect(process.env.AWS_LAMBDA_LOG_GROUP_NAME).to.equal('/aws/lambda/serviceName-dev-hello');
+        expect(process.env.AWS_LAMBDA_LOG_STREAM_NAME)
+          .to.equal('2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad');
+        expect(process.env.AWS_LAMBDA_FUNCTION_NAME).to.equal('serviceName-dev-hello');
+        expect(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE).to.equal('1024');
+        expect(process.env.AWS_LAMBDA_FUNCTION_VERSION).to.equal('$LATEST');
+        expect(process.env.NODE_PATH).to.equal('/var/runtime:/var/task:/var/runtime/node_modules');
+      })
+    );
+
     it('it should overwrite provider env vars', () => {
       awsInvokeLocal.options.functionObj.environment.providerVar = 'providerValueOverwritten';
       return awsInvokeLocal.loadEnvVars().then(() => {