Production-ready lambdas with Node.js

Luciano Mammino (@loige)

2023-04-13

👋 I'm Luciano (🇮🇹🍕🍝🤌)

👨‍💻 Senior Architect @ fourTheorem

📔 Co-Author of Node.js Design Patterns  👉

Let's connect!

  loige.co (blog)

  @loige (twitter)

  mastodon.ie/@loige

  loige (twitch)

  lmammino (github)

Grab the slides

$ ~ whoami

Always re-imagining

We are a pioneering technology consultancy focused on AWS and serverless

✉️ Reach out to us at  hello@fourTheorem.com

😇 We are always looking for talent: fth.link/careers

We can help with:

Cloud Migrations

Training & Cloud enablement

Building high-performance serverless applications

Cutting cloud costs

We host a weekly podcast about AWS

Agenda

  • What is Serverless
  • What is Lambda and how it works
  • Our first Lambda in JavaScript
  • Many tips for making it production ready™️

⚡️Serverless

* Yes, I still have access 😇

*

Serverless, in a nutshell 🥜

  • A way of running applications in the cloud

  • Of course, there are servers
    ... we just don't have to think about them

  • You pay for what you use

  • Small units of compute (functions), triggered by events

Serverless... with benefits 🎁

  • More focus on the business logic (generally)

  • Increased team agility (mostly)

  • Automatic scalability (sorta)

  • Not a universal solution, but it can work well in many situations!

  • Opinion: We will definitely see MOAR SERVERLESS in the future!

AWS Lambda

FaaS offering in AWS

Can be triggered by different kinds of events

  • HTTP Requests
  • New files in S3
  • Jobs in a Queue
  • Orchestrated by Step Functions
  • On a schedule
  • Manually invoked

How Lambda
Works ⚙️

Hello, I am the "Lambda Engine"

Oh, a new event!

Event

Is there a Lambda associated to it?

Event

Yes, here's one! So let's trigger it!

Event

But, we don't have any instance ready!

Event

Let's create a new one!

Event

Let's use the code stored in this bucket!

Event

Let's use the code stored in this bucket!

Event

Now we can let this instance process the event!

Event

Now we can let this instance process the event!

Event

It's working really hard!

Event

🥵 

Nice! Another event!

Event

🥵 

Event

Our only instance is busy... we need a new one!

Event

🥵 

Event

Event

🥵 

Event

Event

🥵 

Event

It can now take care of the event!

Event

🥵 

Event

🥵 

Event

🥳

Event

🥵 

Response

Nice! The first lambda has completed

Event

🥵 

Response

Let's forward its response back!

Event

🥵 

Event

🥵 

The 1st instance has been idle for a while

😴

Event

🥵 

We can destroy it

😴

Event

🥵 

Event

🥳

Response

Nice! The second lambda has completed

Response

Oh, a new event!

Event

There's an instance ready for it!

Event

Event

🥵 

Event

🥳

Response

Response

😴

Summary

  • Events trigger lambda executions
  • Lambda code is stored in an S3 bucket
  • Lambda instances are created and destroyed on demand
  • An instance can handle an event at the time

In more detail 🧐

  • Lambda instances run as micro-vms thanks to Firecracker
  • When an instance is created, this is called cold start
  • Instances can be invoked synchronously (request/response)
  • Or asynchronously
  • An instance doesn't really know how it is invoked, it's just a function
  • It receives an input (the event) and returns an output (the response)

Response

Event

The limits of Lambda 😰

  • 15 minutes execution time limits
  • 256 kb max event/response size (async invocation) *
  • 6 MB max event/response size (sync invocation) *
  • 1000 concurrent executions (soft quota)
  • Max code size: 250 MB

docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html

* With the new Response Streaming feature the response limit goes up to 20 MB (with some extra 💸)

AWS Lambda Pricing 💸

Cost = Allocated Memory 𝒙 time

AWS Lambda Pricing 💸

Cost = Allocated Memory 𝒙 time

512 MB = $0.0000000083/ms

Executing a lambda for 15 mins...

0.0000000083 * 900000 = 0.007 $

AWS Lambda... what about CPU? 🙄

You don't explicitly configure it:

CPU scales based on memory

AWS Lambda... what about CPU? 🙄

You don't explicitly configure it:

CPU scales based on memory

Memory vCPUs
128 - 3008 MB 2
3009 - 5307 MB 3
5308 - 7076 MB 4
7077 - 8845 MB 5
8846+ MB 6

Writing Lambdas

in Node.js

export const handler = async(event, context) => {
  // ... get data from event
  // ... run business logic
  // ... return a result
}
export const handler = async(event, context) => {
  // ... get data from event
  const { url } = event

  // ... run business logic
  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  // ... return a result  
  return res.status
}

We have written our first lambda! 🎉

How do we make it production ready?

1. Use IaC

(Infrastructure as Code 🤗)

1. Why IaC

  • Stop creating resources manually on your AWS accounts, NOW!
  • IaC means that all your infrastructure is defined as code
  • ... which means you can keep it in git
  • ... and create pipelines to deploy changes
  • ... and make all your environments reproducible

1. (some) IaC tools

SAM

Serverless Application Model

AWS SAM

  • YAML-based Infrastructure as code (IaC) tool focused on serverless apps
  • Great when you have to go beyond just one Lambda

Some example use cases:

  • You need to provision multiple lambdas
  • You need to provision multiple pieces of infrastructure (e.g. S3 buckets, permissions, DynamoDB tables, etc)
  • It supports everything that is natively supported by CloudFormation, but with a slightly simpler syntax
  • Deploys through CloudFormation!

AWS SAM

# template.yaml

AWSTemplateFormatVersion : '2010-09-09'
Transform:
  - AWS::Serverless-2016-10-31

Description: |
  A sample Serverless project triggered from S3 CreateObject events
Resources:
  ExampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs18.x
      Handler: index.handler
      Events:
        S3CreateObject:
          Type: S3
          Properties:
            Bucket: !Ref MyPhotoBucket
            Events: s3:ObjectCreated:*

  MyPhotoBucket:
    Type: AWS::S3::Bucket

2. Observability

Observe all the things 👀

(good) Metrics +

2. Observability

Observability Nirvana!

(good) Logs +

(good) Tracing =

Lambda Power Tools!

npm install \
  @aws-lambda-powertools/logger \
  @aws-lambda-powertools/metrics \
  @aws-lambda-powertools/tracer
export const handler = async(event, context) => {
  const { url } = event

  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'

export const handler = async(event, context) => {
  const { url } = event

  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  const { url } = event

  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  const { url } = event

  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  console.info(`Status of ${url} : ${res.status}`)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  return res.status
}
# template.yaml

Type: AWS::Serverless::Function
  Properties:
    # ..
    Environment:
      Variables:
        LOG_LEVEL: DEBUG
import { Logger } from '@aws-lambda-powertools/logger'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })
  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })
  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)

  return res.status
}
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })
  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()

  return res.status
}

Tracing example

Observability is hard!

Can I get something for free, plz? 😩

SLIC Watch

  • SLIC Watch gives you Automatic, best-practice CloudWatch Dashboards and Alarms
  • It works with SAM, CloudFormation, CDK, and Serverless Framework applications.

IaC

SLIC Watch

Deploy to AWS

🤔

🔨🤗

Updated IaC

📈📊🚨

How to use it (with SAM)

Step 1: deploy the CF Transform Macro

👇

# template.yaml

AWSTemplateFormatVersion : '2010-09-09'
Transform:
  - AWS::Serverless-2016-10-31

Description: |
  A sample Serverless project triggered from S3 CreateObject events
Resources:
  ExampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs18.x
      Handler: index.handler
      Events:
        S3CreateObject:
          Type: S3
          Properties:
            Bucket: !Ref MyPhotoBucket
            Events: s3:ObjectCreated:*

  MyPhotoBucket:
    Type: AWS::S3::Bucket

Step 2: Add the SLIC Watch transform

# template.yaml

AWSTemplateFormatVersion : '2010-09-09'
Transform:
  - AWS::Serverless-2016-10-31
  - SlicWatch-v2

Description: |
  A sample Serverless project triggered from S3 CreateObject events
Resources:
  ExampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs18.x
      Handler: index.handler
      Events:
        S3CreateObject:
          Type: S3
          Properties:
            Bucket: !Ref MyPhotoBucket
            Events: s3:ObjectCreated:*

  MyPhotoBucket:
    Type: AWS::S3::Bucket

Step 2: Add the SLIC Watch transform

# template.yaml

AWSTemplateFormatVersion : '2010-09-09'
Transform:
  - AWS::Serverless-2016-10-31
  - SlicWatch-v2
  
Metadata:
  slicWatch:
    enabled: true
    topicArn: !Ref MonitoringTopic

Description: |
  A sample Serverless project triggered from S3 CreateObject events
Resources:
  ExampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs18.x
      Handler: index.handler
      Events:
        S3CreateObject:
          Type: S3
          Properties:
            Bucket: !Ref MyPhotoBucket
            Events: s3:ObjectCreated:*

  MyPhotoBucket:
    Type: AWS::S3::Bucket

Step 3: (optional) add your config

Step 4: deploy!

$ sam deploy

🥳

Without SLIC watch!

With SLIC watch!

With SLIC watch!

With SLIC watch!

With SLIC watch!

With SLIC watch!

3. Avoid code duplication

🛵

Real World™️  lambda

export const handler = (event, context) => {
  // BOILERPLATE!
  // E.g. decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // REAL BUSINESS LOGIC
  const response = doSomethingUsefulWith(event)
  
  // MORE BOILERPLATE
  // E.g. validate output
  // serialize response
  // handle errors
  return response
}

Real World™️  lambda

export const handler = (event, context) => {
  // BOILERPLATE!
  // E.g. decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // REAL BUSINESS LOGIC
  const response = doSomethingUsefulWith(event)
  
  // MORE BOILERPLATE
  // E.g. validate output
  // serialize response
  // handle errors
  return response
}

Lots of boilerplate! 🤷‍♀️

🙇‍♀️

In a real project with many lambdas,
we might end up with a lot of code duplication!

🤔

Can we avoid this clutter by separating
"pure business logic" from the "boilerplate"?

npm install @middy/core

Give it moar love, plz 😍

export const handler = (event, context) => {
  // BOILERPLATE!
  // E.g. decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // REAL BUSINESS LOGIC
  const response = doSomethingUsefulWith(event)
  
  // MORE BOILERPLATE
  // E.g. validate output
  // serialize response
  // handle errors
  return response
}
import middy from '@middy/core'

export const handler = (event, context) => {
  // BOILERPLATE!
  // E.g. decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // REAL BUSINESS LOGIC
  const response = doSomethingUsefulWith(event)
  
  // MORE BOILERPLATE
  // E.g. validate output
  // serialize response
  // handle errors
  return response
}
import middy from '@middy/core'

const lambdaHandler = (event, context) => {
  // REAL BUSINESS LOGIC
  return doSomethingUsefulWith(event)
}

export const handler = (event, context) => {
  // BOILERPLATE!
  // E.g. decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // MORE BOILERPLATE
  // E.g. validate output
  // serialize response
  // handle errors
}
import middy from '@middy/core'

const lambdaHandler = (event, context) => {
  // REAL BUSINESS LOGIC
  return doSomethingUsefulWith(event)
}

export const handler = middy(lambdaHandler)
  .use(/* KMS stuff */)
  .use(/* input deserializer */)
  .use(/* validation */)
  .use(/* auth */)
  .use(/* output serialization */)
  .use(/* error handling */)

A more realistic example
(since Power Tools support Middy...)

import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}
import middy from '@middy/core'
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

export const handler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {
  logger.debug('Received event', { event })
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {
  logger.debug('Received event', { event }) // ❌
  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
Type: AWS::Serverless::Function
  Properties:
    # ..
    Environment:
      Variables:
        LOG_LEVEL: DEBUG
        POWERTOOLS_LOGGER_LOG_EVENT: "true"
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics()
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
  .use(logMetrics(metrics))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  metrics.publishStoredMetrics() // ❌
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
  .use(logMetrics(metrics))
import middy from '@middy/core'
import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'

const logger = new Logger({ serviceName: 'httpCheck' })
const metrics = new Metrics({ namespace: 'utils', serviceName: 'httpCheck' })

const lambdaHandler = async(event, context) => {  
  const { url } = event
  logger.debug('Checking url', { url })

  const res = await fetch(url)
  logger.info('Url status', { url, status: res.status })

  const successMetric = metrics.singleMetric()
  successMetric.addDimension('status', String(res.status))
  successMetric.addMetric('successfulRequests', MetricUnits.Count, 1)
  metrics.addMetric(`successfulRequests${String(res.status)[0]}xx`, MetricUnits.Count, 1)
  return res.status
}

export const handler = middy(lambdaHandler)
  .use(injectLambdaContext(logger))
  .use(logMetrics(metrics))

Middy comes with a built-in collection
of useful middleware

And it's very easy to create your own custom middleware!

Why Middy?

  • Simplify your code
  • Reusability
    • input parsing
    • input & output validation
    • output serialization
    • error handling...
  • Focus (even) MORE on business logic
  • Testability

Let's support Middy development! ❤️ 

4. Performance

⚡️

🔧Fine Tuning

Your lambdas

Fine tuning your Lambdas

  • If you give your lambdas more memory you get more CPU
  • Sometimes having more memory (hence more CPU) can make your executions faster and you can save money
  • So how can we find the sweet spot?!

Lambda Power Tuning

A step function that can test different configurations of your Lambda

5. Other suggestions

🤫

moar tips 🙀

  • use the AWS SDK v3 (more modular, easier to test)
  • use a module bundler (e.g. ESBuild with SAM)
  • use TypeScript (typed events and responses)
  • Re-use objects across invocations (keeping db connections and other shared objects outside the handler)
  • Config through Env Variables
  • Sensitive config through SSM & Secrets Manager

Summary

  • Serverless is great because it let us focus more on what's important for the business!
  • Lambda is a great tool when it comes to writing serverless code
  • We should strive to use IaC to make our deployments versioned & reproducible
  • We can use Power Tools and SLIC Watch to make our apps more observable
  • We can use Middy to improve our code structure
  • We can use Lambda Power Tuning to optimise our Lambda configuration

Cover photo by Jonathan Bean on Unsplash

Thanks to Stella Samuel, Eoin Shanaghy, Will Farrell, and Guilherme Dalla Rosa for the awesome suggestions!

Grab these slides!

Production-ready lambdas with Node.js - jsday 2023

By Luciano Mammino

Production-ready lambdas with Node.js - jsday 2023

A few tips that I learned along the way on how to build production-grade lambdas using AWS: testing, logging, metrics, tracing, infrastructure as code, code reuse, frameworks, and more! Writing Lambdas is quite easy, at the end of the day it is just a function, right?! But writing GOOD production-grade lambdas, well that is an entirely another story. In this talk, I will share some of the tips and tricks that I learned in building Lambda functions in Node.js during the last 6 years. We will talk about code organization and code reuse, logging, metrics, tracing, infrastructure as code, and more.

  • 3,083