Serverless Backend using AWS Lambda: Hands-on Guide
This article was originally written by Rajat S on Medium. Reposted with permission from Bits and Pieces.
Serverless architecture refers to the concept where you give your backend logic to a third party vendor’s server architecture. Doing this allows a developer to focus more on their app and not worry about the server at much.
The word Serverless may misguide one into thinking that there is no server attached to the app. In truth, it just means that the developer does not have to worry about maintaining the backend logic. That responsibility is handed over to the third party vendors like AWS, Azure, and Google.
Serverless Computing can be divided into two variants:
- Backend-as-a-Service (BaaS)
- Function-as-a-Service (FaaS)
With BaaS, the backend logic will be run on a third party vendor. The developer does not need to maintain the servers or the infrastructure which runs the backend services. The developer only needs to pay a subscription to the vendor. The BaaS runs on a shared infrastructure, and the same backend service will be used by multiple applications.
Technologies like AWS Lambda and Microsoft Azure Functions fall under the FaaS category. Here, the developers can implement their own backend logic and run them within the serverless framework. The vendor will handle the operation of the backend logic in a server, along with scalability, reliability, and security aspects.
In this post, we will use AWS Lambda to create a serverless API. We will go through the steps to build the first two endpoints of a REST API and store the data in DynamoDB.
Turn components into APIs with Bit
Bit is an open source tool that turns any reusable piece of JS code into an API which can be discovered, shared and developed from any project. Give it a try.
Getting Started
In order to interact with Amazon Web Services (AWS), we need to get an access key and a secret key along with the permissions to deploy an app.
If you do not already have an account in AWS, create one here. Once you have successfully registered, you will be able to see the AWS Console as shown below:
We can now get the keys and permissions by switching to the identity and access management.
Click on add User
button and create a new user with any User name
that you want to give. Make sure to enable the Programmatic Access. Then click on Next: Permissions.
Select, Attach existing properties directly
and select the AdministratorAccess
.
Click on Next: Review and then Confirm. You will then have created a user in AWS and will now have an Access Key ID and Secret Access Key. This is unique for each user, so I won’t be showing mine to you here.
Now open a command terminal on your computer and install the awscli
. Check out the official docs of AWS CLI to see how to install it depending on your operating system.
Once installed, we need to configure the AWS CLI by running the command:
$ aws configure
The terminal will then ask you to enter the Access Key ID and the Secret Access Key. The terminal will also ask you to enter the Default Region Name and Default Output format. You can leave the last two inputs empty.
Serverless Framework
The serverless
framework allows one to build apps comprised of microservices that run in response to events, auto-scale for you, and only charge you when they run. This lowers the total cost of maintaining your apps, enabling you to build more logic, faster.
Let’s globally install this framework into our system using NPM:
$ npm install -g serverless
Once installed, we can run the serverless
framework in the terminal by running the command:
$ serverless
This command will display all the available commands that come with the serverless
framework.
Note: You can also use the shorthand syntax for serverless
by typing sls
.
Deploy a Node Function to AWS Lambda
In this section, we will see how to configure a Lambda function, add some simple code to it, and deploy it to AWS Lambda.
First let’s create a folder where we can store all the code related to this post.
$ mkdir less-app
You can give any name you that you want to give to this folder. I have named my folder as less-app
for “Serverless-app”.
Next create serverless.yml
file inside this folder.
$ touch serverless.yml
Here we will define a couple of fields such as service
, provider
, and functions
as shown below:
service: less-app provider: name: aws runtime: nodejs8.10 functions: helloWorld: handler: handler.run
Here, the handler
refers to the handler.js
file which in turn needs to export the run
function. We have not yet created this file, so lets do that:
$ touch handler.js
Inside this file, we need to export the run
function. This function has three parameters: event
, context
, and callback
.
The event
is an object that contains all the necessary request data. The context
object contains AWS-specific values like AWS request ID, log group name, and so on. The callback
is a function and should be invoked with an error response as the first argument or a valid response as the second argument.
We can now deploy this run
function using the deploy
command of the serverless
framework as shown below:
$ sls deploy
This command takes a couple of minutes to finish. You should get something like this in your terminal:
Here, the serverless
framework will pack the code into a ZIP file and deploy it with a CloudFormation
stack.
We can now invoke
the Lambda function directly using the invoke
command of the serverless
framework.
$ sls invoke --function helloWorld
Your output should look something like this:
The invoke
command also lets us log out a debug statement. Go to the handler.js
file and insert a console.log
statement as shown below:
module.exports.run = (event, context, callback) => { console.log("Hello World") callback(null, "Hello World") }
To display this statement in our output, we need to add a --log
flag to our invoke
command.
$ sls invoke --function helloWorld --log
Alternatively, we can also run the command sls logs
to print only the past log statements.
$ sls logs --function helloWorld
If you do not want to use the callback
function, you can return a Promise
as well.
module.exports.run = (event) => { return Promise.resolve("Hello world"); }
You might now think that we have to redeploy the whole thing, which takes some time. But we can also just deploy the function
part. This quickens things for us as it skips the CloudFormation
part and replaces the ZIP
for the specific function.
$ sls deploy function --function helloWorld
We can also declare our function as asynchronous and return the desired result.
module.exports.run = async (event) => { return "Hello world"; }
You can re-deploy this function and invoke it, and it should work perfectly.
Attaching HTTP Endpoint to AWS Lambda Function
Using the serverless
framework, we can attach an HTTP endpoint to a Lambda function. To do this, we need to define the path
and method
in the serverless.yml
file.
functions: helloWorld: handler: handler.run events: - http: path: / method: get
But now that we are using HTTP, we need to make a couple of changes to our function because the expected response needs to be an object that contains the statusCode
and the body
. Here, I am going to turn my response into JSON.stringify()
as shown below:
module.exports.run = async (event) => { return{ statusCode: 200, body: JSON.stringify({ message: "Hello World" }) } }
Run the sls deploy
command in the terminal. Unlike previous cases, we cannot add the --function
flag here because the configuration has been changed.
Once deployed, we can use curl
to invoke the published HTTP endpoint.
DynamoDB
DynamoDB is a fully managed NoSQL database service by AWS that provides fast and predictable performance with seamless scalability. It allows us to create a database table that can store and retrieve any amount of data, and serve any level of request traffic.
Lets see how to deploy a DynamoDB table. In order to add a DynamoDB table to our app, we need to add a resources
section to our serverless.yml
file.
resources: Resources: HeroesTable: Type: 'AWS::DynamoDB::Table' Properties: TableName: heroes AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1
We then run sls deploy
command in our terminal. This will tell CloudFormation
to set up our table.
Go to the DynamoDB Console and you will that a table named heroes
has been automatically created. We can add more Attributes to this table later on. Right now, I have only added one attribute named id
to create the table.
Storing Data in DynamoDB
Before we can start storing data in our DynamoDB, we need to set some permissions for the Lambda function to have write access.
Inside the serverless.yml
file, remove the contents of the functions
section completely and create a new function called createHero
as shown below:
functions: createHero: handler: create.run events: - http: path: heroes method: post
But the Lambda function does not have the permissions to interact with our DynamoDB table by default. So we need to give these permissions to our Lambda function. This can be done with the help of iamRole
.
The serverless
framework already uses iamRole
under the hood. We need to use iamRoleStatements
to extend the permissions for the specific iamRole
.
In your DynamoDB console, click on the table to open its overview. In the Table Details section, you should find the Amazon Resourse Name (ARN). Enter its content as Resource
inside the iamRoleStatements
in the serverless.yml
file as shown below:
provider: name: aws runtime: nodejs8.10 iamRoleStatements: - Effect: Allow Action: - dynamodb:PutItem Resource: "<enter your Amazon Resource Name here>"
We also have to create the function that will actually send the data to our DynamoDB table. So create a new file named create.js
.
$ touch create.js
Inside this file, add a function that will return a response.
module.exports.run = async (event) => { const data = JSON.parse(event.body); return { statusCode: 200, body: JSON.stringify(data) }; };
To let this function interact with DynamoDB, we need to import the aws-sdk
and instantiate a documentClient()
inside the serverless.yml
file:
const AWS = require("aws-sdk"); const client = new AWS.DynamoDB.documentClient();
Along with this, create a new const named params
as shown below:
module.exports.run = async (event) => { const data = JSON.parse(event.body); const params = { TableName: "heroes", Item: { id: "hero1", name: data.name, checked: false } }; await client.put(params).promise(); return { statusCode: 200, body: JSON.stringify(data) }; };
There is one issue in this code, every new item that we add to our table will have the same id
. To solve this, we can add the uuid
package to our code. This package will generate a new id
for every new request.
Create a new file named package.json
:
$ touch package.json
Inside this file, write the following code:
{ "name": "less-app", "private": true }
Then install the uuid
package using NPM/Yarn:
$ npm install --save uuid
Finally, import this package into the create.js
file:
const uuid = require("uuid/v4");
Also re-write the id
property inside the Item
object using the uuid
package as shown here:
Item: { id: uuid(), name: data.name, checked: false }
With that, we have set up everything and can redeploy this using serverless
.
$ sls deploy
Once deployed we can use curl
to create an entry into the DynamoDB table as shown below:
$ curl -X "<POST URL>" --data '{"text": "batman"}'
Check in the AWS console to see if this submission was recorded.
Conclusion
I hope this post helped you understand the basics of AWS Lambda and understand why serverless architecture is worth trying out.
Serverless Architecture like AWS Lambda will simplify the maintenance of backend systems while giving cost benefits for handling all sorts of different user behaviors.
You should especially consider implementing serverless architecture if you have a small number of functions that you need hosted. If your application is more complex, a serverless architecture can still be beneficial, but you will need to build your app in a different way.
This may not be feasible if you have an existing application. It may make more sense to migrate pieces of the application into serverless functions over time.
Further Reading
- Monorepos Made Easier with Bit and NPM
- How To Share React UI Components Between Projects And Apps
- How to Avoid Duplicate Code with Git+Bit