Testing serverless code with serverless

Testing serverless code with serverless

Recently, I was assigned a task, to figure out how to run serverless code in local environment. I actually seldom code for the most of my time, and my knowledge of running an app is with command like node app.js or python main.py, and the interpreter will stay there, listening for connection on some random port, then I would use curl or something to reach endpoint. But that’s not how serverless worked.

What is serverless?

When we talk about “serverless”, we must also mention AWS Lambda, AWS Lambda is something called “serverless computing”.

Before we explain what is “serverless computing”, let’s think how we deploy our code.

Usually we have some code, whether it’s Python or PHP, then we need to use some kind of program to “serve” our code, like php-fpm or uWSGI, things like that. Then we would need a web server, to proxy client’s connection to the program, so the request can reach our code, then our code can respond to that request.

Now, imagine a service, that you only need to provide your code, select the environment, then the service will take care “everythig”, spawning instance, running your code, providing endpoint, auto-scaling…and much more. Isn’t it terrific? Developer won’t need to worry about failing servers, they can focus on their code.

This is basically “serverless computing”. You provide your code, then someone run that code for you, maintain the server for you, scale your app for you.

AWS Lambda is the first cloud platform that provides serverless computing, and people started noticing this concept. It’s easier to deploy small-sized code, and it’s easy to use. AWS Lambda can also be used with AWS API Gateway, so you can control your application API.

How serverless work?

“serverless” would require you to provide your code, then select your environment, right? For most of the serverless service, or so called FaaS (function as a service) out there, this is how they manage your code.

  1. Select the base container image using the environment you chosed.
  2. Insert the code you provided into the image, then create a new image.
  3. Spawn multiple container with new image, then enable a port or socket, so the code inside the container can communicate with outside.
  4. Some kind of load balancer will create a new route to those containers, and you get an endpoint, which can reach your code via services they created.

There’s a major difference between deploying code the old fashion way and using serverless computing. Serverless computing is stateless, since you don’t know when or why the service provider is going to shut down the container, or generate a new container, or even route your request to different container.

Serverless example

Below is a sample code, which can be deployed on FaaS platform.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

module.exports.hello = async (event, context, callback) => {
let name = "World";
if(event.name)
name = event.name;
const response = {
statusCode: 200,
body: JSON.stringify({
message: `Hello ${name}!`
})
};

callback(null, response);
}

Now here comes the question, if I want to test this code, how can I run it locally?

You can’t run this code directly, it would terminate as soon as it starts, since there are no request to the code. The only way to test it is to deploy the code and test on the cloud, or is it?

Enter the application serverless

What is serverless, again

serverless is a framework written in Node.js. Just like it’s name, it’s very helpful when developing serverless code. It can help you build, test and deploy your code, all in one package.

You need to write serverless.yml, it’s config file, in this file you can define the function, set up deployment, and much more.

Now let’s say we want to run the code above in local environment, how can we do that?

First, install serverless with npm install serverless -g. Then save the YAML config below as serverless.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
service: 'serverless-sample'

plugins:
- serverless-offline

provider:
name: aws
runtime: nodejs12.x

functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: POST

Now, we can use the command below to trigger this function.

1
$ serverless invoke local -f hello

You should see output like this

1
2
3
4
{
"statusCode": 200,
"body": "{\"message\":\"Hello World!\"}"
}

If you have read the code above, you should notice something called event, this is the variable where client can pass it’s request into the function. For the code above, you can pass a data with variable name to change the output. Let’s give it a shot.

1
$ serverless invoke local -f hello -d '{"name": "John"}'
1
2
3
4
{
"statusCode": 200,
"body": "{\"message\":\"Hello John!\"}"
}

Now, the invoke local command is really convenient, but I would want to have a HTTP endpoint instead of triggering the function via command-line, how can I do that?

serverless-offline

serverless-offline is a plugin for serverless, it can emulate an AWS Lambda and AWS API Gateway envioronment locally, so you can test your code just like using AWS services.

Install the package with npm install serverless-offline, then add code below into serverless.yml

1
2
plugins:
- serverless-offline

You can use serverless --verbose to verify if serverless-offline plugin is loaded or not.

After adding the line, now you should be able to test you code using HTTP endpoint, just run

1
$ serverless offline

You should see output like

1
2
3
4
5
6
7
8
9
10
11
12
13
offline: Starting Offline: dev/us-east-1.
offline: Offline [http for lambda] listening on http://localhost:3002

┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ POST | http://localhost:3000/dev/hello │
│ POST | http://localhost:3000/2015-03-31/functions/hello/invocations │
│ │
└─────────────────────────────────────────────────────────────────────────┘

offline: [HTTP] server ready: http://localhost:3000 🚀
offline:
offline: Enter "rp" to replay the last request

Now, try to reach HTTP endpoint using cURL

1
$ curl -X POST -H 'content-type: application/json' http://localhost:3000/dev/hello

Then you can see the result

1
{"message":"Hello World!"}

Now try to reach HTTP endpoint with data using cURL

1
$ curl -X POST -H 'content-type: application/json' -d '{"name": "John"}' http://localhost:3000/dev/hello

The name didn’t change! Why?

After some digging, I found that when you’re emulating AWS environment locally, it would also emulate a feature called Lambda Proxy. It’s a feature that would pass more information into your code. For example, if I send {"name": "John"} as data with Lambda Proxy off, the code would receive {"name": "John"}, but if Lambda Proxy is enabled, it would receive…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
{
"body": "{\"name\": \"John\"}",
"headers": {
"Host": "localhost:3000",
"User-Agent": "curl/7.71.1",
"Accept": "*/*",
"content-type": "application/json",
"Content-Length": "16"
},
"httpMethod": "POST",
"isBase64Encoded": false,
"multiValueHeaders": {
"Host": [
"localhost:3000"
],
"User-Agent": [
"curl/7.71.1"
],
"Accept": [
"*/*"
],
"content-type": [
"application/json"
],
"Content-Length": [
"16"
]
},
"multiValueQueryStringParameters": null,
"path": "/hello",
"pathParameters": null,
"queryStringParameters": null,
"requestContext": {
"accountId": "offlineContext_accountId",
"apiId": "offlineContext_apiId",
"authorizer": {
"principalId": "offlineContext_authorizer_principalId"
},
"domainName": "offlineContext_domainName",
"domainPrefix": "offlineContext_domainPrefix",
"extendedRequestId": "ckcgitlev0000umwybeumhooj",
"httpMethod": "POST",
"identity": {
"accessKey": null,
"accountId": "offlineContext_accountId",
"apiKey": "offlineContext_apiKey",
"caller": "offlineContext_caller",
"cognitoAuthenticationProvider": "offlineContext_cognitoAuthenticationProvider",
"cognitoAuthenticationType": "offlineContext_cognitoAuthenticationType",
"cognitoIdentityId": "offlineContext_cognitoIdentityId",
"cognitoIdentityPoolId": "offlineContext_cognitoIdentityPoolId",
"principalOrgId": null,
"sourceIp": "127.0.0.1",
"user": "offlineContext_user",
"userAgent": "curl/7.71.1",
"userArn": "offlineContext_userArn"
},
"path": "/hello",
"protocol": "HTTP/1.1",
"requestId": "ckcgitlew0001umwyf49m3xvn",
"requestTime": "11/Jul/2020:01:55:42 +0800",
"requestTimeEpoch": 1594403742602,
"resourceId": "offlineContext_resourceId",
"resourcePath": "/dev/hello",
"stage": "dev"
},
"resource": "/dev/hello",
"stageVariables": null
}

and the data would be in event.body, so if you want to test your code locally using HTTP endpoint, you need to modify your code to use event.data instead of using event directly.

The new code should look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';

module.exports.hello = async (event, context, callback) => {
console.log(JSON.stringify(event));
let name = "World";
if(event.body){
let body = JSON.parse(event.body)
if(body.name)
name = body.name;
}
const response = {
statusCode: 200,
body: JSON.stringify({
message: `Hello ${name}!`
})
};

callback(null, response);
}

Now test the code using serverless offline again, with data, you should see the correct result.

It’s easy, right?