Friday, 13 June 2025

Introduction to Serverless Framework



Serverless Framework is a tool designed to streamline the development and deployment of serverless applications, including functions and infrastructure, by abstracting away the need to manage servers. 

We define desired infrastructure in serverless yaml files and then deploy it by executing:

sls deploy

This command parses serverless yaml file into larger AWS CloudFormation template which automatically gets filled with values from the yaml. 


Serverless Yaml Configuration File


serverless yaml file defines a serverless service. It is a good idea to break up the serverless project into multiple services, each of which is defined by its own serverless yaml file. We don't want to have everything in one big infrastructure stack. 

Example:
  • database e.g. DynamoDB
  • Rest API e.g. which handles the submitted web form and stores data in DynamoDB
  • front-end website which e.g. stores React app website in s3 bucket

Services can be deployed in multiple regions. (Multi-region architecture is supported)


serverless.yml example:


service: my-service
frameworkVersion: "3"
useDotenv: true
plugins: 
  - serverless-plugin-log-subscription
  - serverless-dotenv-plugin
provider:
  name: aws
  runtime: nodejs14.x
  region: eu-east-1
  memorySize: 512
  timeout: 900
  deploymentBucket:
    name: my-serverless-deployments
  vpc: 
    securityGroupIds: 
      - "sg-0123cf34f6c6354cb"
    subnetIds: 
      - "subnet-01a23493f9e755207"
      - "subnet-02b234dbd7d66d33c"
      - "subnet-03c234712e99ae1fb"
  iam: 
    role:
      statements:
        - Effect: Allow
          Action:
            - lambda:InvokeFunction
          Resource: arn:aws:lambda:eu-east-1:123456789099:function:my-database
package:
  patterns:
    - "out/**"
    - "utils.js"
    - "aws-sdk"
functions:
  my-function:
    handler: lambda.handler
    events:
      - schedule:
          name: "my-service-${opt:stage, self:provider.stage}"
          description: "Periodically run my-service lambdas"
          rate: rate(4 hours)
          inputTransformer:
            inputTemplate: '{"Records":[{"EventSource":"aws:rate","EventVersion":"1.0","EventSubscriptionArn":"arn:aws:sns:eu-east-1:{{accountId}}:ExampleTopic","Sns":{"Type":"Notification","MessageId":"95df01b4-1234-5678-9903-4c221d41eb5e","TopicArn":"arn:aws:sns:eu-east-1:123456789012:ExampleTopic","Subject":"example subject","Message":"example message","Timestamp":"1970-01-01T00:00:00.000Z","SignatureVersion":"1","Signature":"EXAMPLE","SigningCertUrl":"EXAMPLE","UnsubscribeUrl":"EXAMPLE","MessageAttributes":{"type":{"Type":"String","Value":"populate_unsyncronised"},"count":{"Type":"Number","Value":"400"}}}}]}'
      - sns:
          arn: arn:aws:sns:us-east-2:123456789099:trigger-my-service
      - http: 
custom:
  dotenv:
    dotenvParser: env.loader.js
  logSubscription:
      enabled: true
      destinationArn: ${env:KINESIS_SUBSCRIPTION_STREAM}
      roleArn: ${env:KINESIS_SUBSCRIPTION_ROLE}



  • provider
    • vpc
      • securityGroupIds 
      • subnetIds - typically a list of private subnets with NAT gateway. 
  • functions - defines the AWS Lambda functions that are deployed as part of this Serverless service. This is where we define the AWS Lambda functions that our Serverless service will deploy. 
    • <function_name> (e.g., my-function) Each function entry under functions specifies:
      • handler - tells Serverless which file and exported function to execute as the Lambda entry point (e.g., lambda.handler)
      • events - a list of events that trigger this function, such as:
        • schedule: for periodic invocation (cron-like jobs)
        • sns: for invocation via an AWS SNS topic
        • http


Lambda in VPC


A Lambda function needs to be associated with a subnet only when we configure it to run inside a VPC (Virtual Private Cloud). When we enable VPC access for the Lambda function we must specify:
  1. at least one subnet (for ENI creation)
  2. security groups (for network rules)

When a Lambda is in a VPC AWS creates an Elastic Network Interface (ENI) in the specified subnet. That ENI allows our Lambda to access private resources, like:
  • RDS databases
  • EC2 instances
  • Internal APIs
  • Private endpoints (e.g., S3 via VPC endpoint)

If our Lambda doesn’t need to access private resources, we usually don’t need to assign a VPC.
Assigning a Lambda to a VPC removes its default internet access — unless:
  • The subnet is public (with a route to an internet gateway), or
  • We have a NAT Gateway in a public subnet, and route private subnets to it


Handler


lambda.handler in the serverless.yml file tells us which code to execute when the Lambda function is triggered.

Specifically, lambda.handler means:
  • The Lambda function’s code entry point is in a file named lambda.js (or lambda/index.js if it’s a directory named lambda).
  • The exported function named handler inside that file will be invoked by AWS Lambda.

So, AWS Lambda will look for a file called lambda.js (or lambda/index.js), and inside it, the function handler should be exported, for example:


// lambda.js
exports.handler = async (event) => {
  // our code here
};


The name of the deployed Lambda function will be constructed using the following pattern:

<service-name>-<function-name>-<stage>

Based on our serverless.yml:
  • service name: my-service
  • function name: my-function
  • stage: If not explicitly defined, it defaults to dev (or whatever is provided at deploy time with --stage).

So, the default deployed Lambda function name will be:

my-service-run-dev

If we deploy with a different stage (e.g., --stage production), the name would be:

my-service-run-production

We can always override this default by providing a name property under the function's configuration, but with our current config, the above naming applies.

npm i -g serverless
npm install
sls deploy --stage <env>


Serverless State


Serverless yaml can define an output, e.g. in ServiceA

service: service-a
custom:
   env: test
   db_name: my_db_${self:custom.env}
outputs:
    db_name: ${self.custom.db_name}

If we have multiple services within the app (multiple serverless yamls), we can share the state of each service if we add to each yaml file an attribute app, set to the same value (app name). If we have both in ServiceA and ServiceB e.g.

app: demo

Then ServiceB can read the state from ServiceA like:

service: service-b
...
${state:service-a.db_name}
...


Testing Lambda Function Locally


The command:

sls invoke local --function main --path tests/test_payload.json 

...invokes a Serverless function named "main" locally, using the event payload defined in the tests/test_payload.json file. This is a convenient way to test Lambda functions without deploying them to AWS. 

  • sls invoke local: This invokes the specified function locally, simulating the AWS Lambda environment.
  • --function main: Specifies the function to be invoked locally as "main".
  • --path tests/test_payload.json: Indicates the path to a JSON file containing the event data to be passed to the function. This event data will be used as input to the function when it runs locally. 



Resources:


No comments: