Friday, February 18, 2022

Serverless Notes for AWS

Serverless Essentials

Serverless notes

Deploying AWS Lambda

Here we are going to see the steps needed to create our first lambda and deploy using serverless.

Below are the minimum needed for a serverless.yml config file to deploy a simple lambda function.

Note: To use serverless we need to install serverless cli and aws cli and configure the access key and secret key of aws into ~/.aws/credentials file

serverless.yml

service: sls-test
provider:
    name: aws
    runtime: nodejs12.x
    region: ap-southeast-1
    stage: dev [dev is default if not specified]
functions:
    helloLambda:
	  handler: index.handler

index.js

async  function  hello(event, context) {
  return {
    statusCode:  200,
    body: JSON.stringify({ success:  true, message: "hello world"}),
  };
}

module.exports.handler = hello;

Here hello function is exported as handler from index.js

we can deploy the code using command sls deploy --verbose

This will create lambda function in AWS with name sls-test-lambda-dev-helloLambda . Here service name + stage + functionName (given in yml file) is taken to form the name of the lambda function.

Testing

We can test the function by using sls invoke command

sls invoke -f helloLambda

This will execute the function and provide the output in command line. We can also go to the aws console and test the function.

sls invoke -f helloLambda -l will give us the logs from cloud watch during the execution time

sls logs -f helloLambda will give us the complete cloud watch logs for this function.

sls logs -f helloLambda --startTime 1m will give the last 1 minute logs. You also try hrs.

Connecting API Gateway

We can attach API gateway as trigger point for the lambda so we can get API url using which the function can be called. Here is the serverless.yml config will look like

API Gateway lets you deploy HTTP APIs. It comes in two versions:

  • v1, also called REST API
  • v2, also called HTTP API, which is faster and cheaper than v1

serverless.yml using v1

service: sls-test
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-southeast-1
  stage: dev [dev is default if not specified]
functions:
  helloLambda:
    handler: index.handler
    events:
      - http:
          method: get
          path: /hello

This will create a rest end point for GET method using /hello as route. There is also another event type called httpApi which will create http end point. Example is below for v2 type of http end point.

We can still use the same v1 syntax by replacing http with httpApi and keeping method and path as child to that, else we can have short syntax as below.

functions:
  helloLambda:
    handler: index.handler
    events:
      - httpApi: 'GET /hello'

How to include dependencies

We often use npm modules as dependencies while writing code. In general AWS Lambda can execute the code what we give, we cannot perform npm install inside the Lambda. So basically the solution is to either

  • Upload the full project (including node_modules folder)
    (or)
  • Compile the code with webpack or parcel and upload the compiled version of the code

serverless helps to do both of them. Lets see with an example.

Example1

We are going to include axios module as our npm dependency and using axios we are going to get data from public end point and return the result as Lambda output.

Here is the same index.js code (in common js format) including the axios module

const  axios = require("axios");

async  function  hello(event, context) {
  const { data } = await  axios.get("https://jsonplaceholder.typicode.com/todos/1");
  console.log(data);
  return {
    statusCode:  200,
    body: { success:  true, data },
  };
}

module.exports.handler = hello;

Then we just need to do sls deploy --verbose to update the function in AWS. Now if you go to the AWS console, you can see the node_modules folder is also present (to support axios package). it will work just fine.

If we want to do this without serverless, then we take zip file of the code and upload into Lambda using AWS cli. Refer more here

The following update-function-code example replaces the code of the unpublished ($LATEST) version of the my-function function with the contents of the specified zip file.

aws lambda update-function-code \
    --function-name  my-function \
    --zip-file fileb://my-function.zip

Example2

Here we are going to use the power of webpack. This will compile all our code and create single js file that will be uploaded into AWS Lambda.

If we configure webpack by our own it will require more config. So we are going to serverless plugin called serverless-bundle Read more about it here

To use this plugin we need to first install it as dev dependency like

npm install --save-dev serverless-bundle

then modify serverless.yml to include one more section like below.

plugins:
  - serverless-bundle

With this one change, we do no need any more config and try to deploy the function and see the magic.

After running sls deploy You can see the Lambda function in AWS now contains only one js file that is totally unreadable for us. But the node environment will be able to read and deliver exactly what we asked for.

Since we are using webpack now, we can extend our code to include all the latest ES6+ syntax like below.

index.js

import axios from 'axios';

async  function  hello(event, context) {
  const { data } = await  axios.get("https://jsonplaceholder.typicode.com/todos/1");
  console.log(data);
  return {
    statusCode:  200,
    body: { success:  true, data },
  };
}

export const handler = hello;

Create DynamoDB using Serverless

Serverless uses cloudformation to do things. So we can pretty much do everything what cloudformation will do. So we can create AWS resources on the fly using serverless.

How Serverless works

The framework will look for serverless.yml file and its configuration and upload your files into S3 and from there it will run cloudformation stack. So once the deployment is completed you can go to the S3 and look for files that was used by serverless to invoke the cloudformation.

If you go to cloudformation then you will be able to see the stack and how many resources were created by them.

So all the AWS resources we want to create will go under resources section of the serverless.yml file. Here is an example of creating a DynamoDB table.

Keep in mind, resources is the serverless section and then what comes under is full of cloudformation syntax.

resources:
  Resources:
    tasksTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

How to provide IAM roles for Lambda to access DynamoDB

If you worked in AWS you know that by default resources created in AWS does not have permission on other AWS resources. So if we create a Lambda function and if we want the function to connect to DynamoDB and get the data then the Lambda should have permission to do that.

Normally Lambda will be created with a role. The role can contain policies that gives permission for the Lambda to use other AWS resources. If we create the lambda function using AWS console, we manually create these policies and attach with the lambda. With serverless we need a way such that the lambda will be created in AWS along with the required permission.

This can be achieved by giving the IAM statements in iamRoleStatements section.

Below is an example statement. Under the section it takes array of statements. Each statement consist of Effect, Action and Resource details.

Below one gives Allow permission to all dynamodb* actions (API like get, put etc…) to two resources. Resources are specified using ARN. Second one is direct hard coded value but the first one is kind of taken from configuration. See below on how it works.

provider:
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:*
      Resource:
        - arn:aws:dynamodb:${self:provider.region}:${self:custom.accountId}:table/${self:custom.tableName}
        - arn:aws:dynamodb:ap-southeast-1:123456789012:table/users

custom:
  accountId: ${aws:accountId}
  tableName: TasksTable-${self:provider.stage}

Other Important serverless.yml config

Custom section

custom section is the section where we can provide our own key value pair values that can be used within serverless.yml file as configurations

The way how we can refer to the custom values are using below yml syntax. self refers to the current file then followed by

${self:custom.tableName}

Environment variable

We often want the program to take the environment specific values from environemnt variables. In Node this can be accessed in code via process.env.ENV_VAR_NAME

In AWS console under each Lambda we can manually enter the environment value as key-value pair. In serverless the same can be done using provider.environment section.

We can either give the value directly or can refer from other values (like custom section)

provider:
  environment:
    TASK_TABLE_NAME: ${self:custom.tableName}
    KEY: SOME_VALUE
	
custom:
  tableName: TasksTable-${self:provider.stage}

Command line options

During execution of sls deploy command we can specify the options in run time using -- prefix. E.g. if we want to set the stage to sit or prod then we can do so by

sls deploy --verbose --stage prod

We can get access to this value in serverless.yml using below syntax. opt refers to the command line options. Below example takes the value from opt:stage if not provided it defaults to ‘dev’

provider:
  stage: ${opt:stage, 'dev'}

Splitting the serverless.yml

In some cases, if your serverless.yml file is getting bigger, it is possible to split. Meaning that creating separate yml file for some section and refer that file inside serverless.yml.

Lets us take an example below. We are referring hello function in serverless.yml based on the helloFunction section inside that hello.yml

serverless.yml

functions:
  hello: ${file(functions/hello.yml):helloFunction}
  welcome:
    handler: handler.welcome
    events:
      - http:
          method: GET
          path: /welcome

functions/hello.yml

Should be placed inside the functions folder and it should be in same level with serverless.yml

helloFunction:
    handler: handler.hello
    events:
      - http:
          method: GET
          path: /hello