Structuring Services for AWS Lambda with Serverless

Harsh Singhal
6 min readDec 30, 2017

--

When I started with AWS Lambda for the first time, I used to make .zip file to package all my dependencies and upload at the AWS console to test my lambda function and when I needed to change anything, I would update my code and repackage and upload it to AWS console, as it was very time consuming process hence I started with AWS CLI to package and upload my code through the shell script.

For checking my logs I was directed to cloudwatch, and for setting path and domain, I needed to go to the API Gateway. There was no other way until I got to know about this framework called “Serverless”.

Serverless gave me a push to speed up my development process, now I can deploy all my lambdas at once or singularly, and I could check all my logs in the terminal.

API Gateway setup with custom domain can be done here, and setting the authorizer function which can be used to authorize every request before hitting the main lambda is pretty easy and convenient, being it set up IAM permissions or environment variables for lambda that can be stored in a secret file without exposing it to the repository, while syncing up with other team members using encrypted secret file.

So let me share my knowledge, how you can also structure your own Serverless application with various event triggers to achieve agility in your development with NodeJS6.03 runtime.

First install Serverless globally in your system.

npm install serverless -g

Go inside the root folder of your application

Create your first Serverless service by running the below command in terminal.

serverless create --template aws-nodejs --path UserService

alternatively the below can be used…

sls create --template aws-nodejs --path UserService

One folder “UserService” will be generated with files serveress.yml, handler.js and .gitignore.

serverless.yml contains information about infrastructure to be created like all functions details and their handlers, packaging information, event mapping, domain mapping and IAM permissions etc. handler.js contain function hello exported by default for lambda proxy integration with API Gateway. You can export more functions from handler.js and write handler details in serverless.yml file unless you want to package each lambda separately to package with their own dependency, so that different team members can effectively work on different lambda without merge issues. What I prefer is packaging lambda separately.

To package lambda separately you need to set up the package property in serverless.yml file to exclude all files and folder that is common for all functions, and set up package property inside each function to package only their folder like below template

package:
individually: true
exclude:
- ./**

Inside each functions set package property as for Login function

package:
include:
- Login/index.handler

here Login/index.handler means you have Login folder under your UserService folder and inside Login, index.js is there which is exporting function “handler” like below code

UserService/Login/index.js

Now to invoke lambda function when user hit a url, you need to use API Gateway where you can create api with different path associated with different lambda function. As I told previously it was tough job for me to add every path in console and wait for successfully added to proceed for next step, now using serverless.yml file, I can define my all mapped path with lambda functions. For that I just need to set events property nesting with http key like below template

functions:
getprofile:
handler: GetProfile/index.handler
description: get profile of user
events:
- http:
path: /getprofile
method: get
private: false
authorizer:
arn: arn:aws:lambda:us-east-1:AWS_ACCOUNT_ID:function:UserService-dev-authorizer
resultTtlInSeconds: 60
identitySource: method.request.header.x-access-token
type: token
package:
include:
- GetProfile/**

In API Gateway, you can set authorizer function that will check user’s authorization before hitting main Lambda and pass user details(email, username) to main lambda, so your main lambda will only perform business logic rather than messing up with checking user authentication. If your app has enough lambdas and user authorization is there for most of the operations, then its better choice to use authorizer than copying the same code in every lambda. And when some changes is required in authentication flow you need to change only one rather than all. This can be done using any of the request parameter like header, by default it is done by “Authorization” header by passing bearer token.

Best part is this can be done from here without much effort. This can be done by configuring event property in serverless.yml file inside functions like above template for getprofile function in AccountService.

Here getprofile function use authorizer by arn, it can be done by using name if it is on same service. resultTtlInSeconds property define cache time for authorize request( default is 300) in seconds, like if user A send request and successfully authenticated for the first time, so the authorizer function will not hit for further request in 60 seconds and directly invoke main lambda function by passing the same user detail, if user pass same token for next 60 seconds in request header.

Using identitySource you configure the header key for passing the authentication token, by default it is Authorization, I made it ‘x-access-token’ as you can see in above template.

Authorizer function checks for user authentication and generate IAM policy for user and give permission to invoke lambda function, just after that lambda function invoked with rest of the request parameter.

Authorizer function look like below code

UserService/Authorizer/index.js

Here I am using mongodb as my database, checking whether user is authenticated by token, and calling the generatePolicy function if authenticated as below

function generatePolicy under “UserService/Authorizer/index.js”

and in getprofile function i am extracting data using event.requestContext.authorizer like in below code

AccounService/GetProfile/index.js

likewise for other routes also, you can configure the same, here one thing is common, I am using environment variable for all of my function to get mongodb connection url, that I have set in yml file which is not even hardcoded for every service, I referred to a common file in root folder so that if I change my mongodb connection url, it will reflect in every lambda function. Again the problem arises that if I change some configs in secrets file it is not reflected to my team member’s directory because of setting this file in .gitignore.For that I used a plugin “serverless-secrets-plugin” which encrypt my secret file with a particular password, that encrypted file we can push to repository and share password with team members, so they can decrypt and get the same config, this plugin need to install using npm in the current directory of service

Now everything was set except I wanted to deploy my lambdas directly to api.mywebsite.com rather than some random url for every other service, and apply for different stage like dev, staging and prod with dev.api.mywebsite.com and staging.api.mywebsite.com so it can go through certain process to finally go for production. For that I used a plugin “serverless-domain-manager” with setting the property customDomain in custom in serverless.yml file, also the certificate for those domain should be registered with ACM(Amazon Certificate Manager). template for setting this looks like below

custom:
stage: ${opt:stage, self:provider.stage}
domains:
prod: api.yourwebsite.com
staging: staging.api.yourwebsite.com
dev: dev.api.yourwebsite.com
customDomain:
basePath: "user"
domainName: ${self:custom.domains.${self:custom.stage}}
stage: "${self:custom.stage}"
createRoute53Record: true

Here basePath user implies that for dev stage all function url inside UserService will be available at dev.api.mywebsite.com/user/

Complete sample code is available on Github:

https://github.com/Harsh0/Serverless-multiservice-architecture

Looking forward to hear your feedback

--

--