How to use Lambda Layers in your serverless app
In this example we will look at how to use Layers in your serverless app with SST. We’ll be using the @sparticuz/chromium Layer to take a screenshot of a webpage and return the image in our API.
We’ll be using SST’s Live Lambda Development. It allows you to make changes and test locally without having to redeploy.
Requirements
- Node.js 16 or later
- We’ll be using TypeScript
- An AWS account with the AWS CLI configured locally
Create an SST app
Let’s start by creating an SST app.
$ npx create-sst@latest --template=base/example layer-chrome-aws-lambda
$ cd layer-chrome-aws-lambda
$ npm install
By default, our app will be deployed to an environment (or stage) called dev
and the us-east-1
AWS region. This can be changed in the sst.config.ts
in your project root.
import { SSTConfig } from "sst";
import { Api } from "sst/constructs";
export default {
config(_input) {
return {
name: "layer-chrome-aws-lambda",
region: "us-east-1",
};
},
} satisfies SSTConfig;
Project layout
An SST app is made up of two parts.
-
stacks/
— App InfrastructureThe code that describes the infrastructure of your serverless app is placed in the
stacks/
directory of your project. SST uses AWS CDK, to create the infrastructure. -
packages/functions/
— App CodeThe code that’s run when your API is invoked is placed in the
packages/functions/
directory of your project. -
layers
- Lambda LayersThe code that’s run when your API is invoked is placed in the
packages/functions/
directory of your project.Creating the API
Let’s start by creating our API.
Replace the stacks/ExampleStack.ts
with the following.
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Api, StackContext } from "sst/constructs";
export function ExampleStack({ stack }: StackContext) {
const layerChromium = new lambda.LayerVersion(stack, "chromiumLayers", {
code: lambda.Code.fromAsset("layers/chromium"),
});
// Create a HTTP API
const api = new Api(stack, "Api", {
routes: {
"GET /": {
function: {
handler: "packages/functions/src/lambda.handler",
// Use 18.x here because in 14, 16 layers have some issue with using NODE_PATH
runtime: "nodejs18.x",
// Increase the timeout for generating screenshots
timeout: 15,
// Load Chrome in a Layer
layers: [layerChromium],
// Exclude bundling it in the Lambda function
nodejs: {
esbuild: {
external: ["@sparticuz/chromium"],
},
},
},
},
},
});
// Show the endpoint in the output
stack.addOutputs({
ApiEndpoint: api.url,
});
}
Here, we will download the Layer from the Sparticuz/chromium GitHub release. Create folder layers/chromium
in the root of the project and unzip the file layer then you will have layers/chromium/nodejs
. The nodejs
folder contains the node_modules
folder and the package.json
file.
We then use the Api
construct and add a single route (GET /
). For the function that’ll be handling the route, we increase the timeout, since generating a screenshot can take a little bit of time. We then reference the Layer we want and exclude the Lambda function from bundling the @sparticuz/chromium npm package.
Finally, we output the endpoint of our newly created API.
Adding function code
Download chromium locally, then you will have YOUR_LOCAL_CHROMIUM_PATH. You will need it in lambda function to run chromium.
$ npx @puppeteer/browsers install chromium@latest --path /tmp/localChromium
Now in our function, we’ll be handling taking a screenshot of a given webpage.
Replace packages/functions/src/lambda.ts
with the following.
import puppeteer from "puppeteer-core";
import chromium from "@sparticuz/chromium";
// chrome-aws-lambda handles loading locally vs from the Layer
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
// This is the path to the local Chromium binary
const YOUR_LOCAL_CHROMIUM_PATH = "/tmp/localChromium/chromium/mac-1165945/chrome-mac/Chromium.app/Contents/MacOS/Chromium";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
// Get the url and dimensions from the query string
const { url, width, height } = event.queryStringParameters!;
if (!url) {
return {
statusCode: 400,
body: "Please provide a url",
};
}
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: process.env.IS_LOCAL
? YOUR_LOCAL_CHROMIUM_PATH
: await chromium.executablePath(),
headless: chromium.headless,
});
const page = await browser.newPage();
if (width && height) {
await page.setViewport({
width: Number(width),
height: Number(height),
});
}
// Navigate to the url
await page.goto(url!);
// Take the screenshot
const screenshot = (await page.screenshot({ encoding: "base64" })) as string;
const pages = await browser.pages();
for (let i = 0; i < pages.length; i++) {
await pages[i].close();
}
await browser.close();
return {
headers: { "Content-Type": "text/plain" },
body: "Screenshot taken",
};
};
First, we grab the webpage URL and dimensions for the screenshot from the query string. We then launch the browser and navigate to that URL, with those dimensions and take the screenshot.
Now let’s install the npm packages we need.
Run the below command in the packages/functions/
folder.
Please check Puppeteer’s Chromium Support page and install the correct version of Chromium. At the moment writing this tutorial, puppeteer-core@20 is compatible with Chromium@113 is most stable.
$ npm install puppeteer-core^20.1.2 @sparticuz/chromium^113.0.1
Starting your dev environment
SST features a Live Lambda Development environment that allows you to work on your serverless apps live.
$ npm run dev
The first time you run this command it’ll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment.
===============
Deploying app
===============
Preparing your SST app
Transpiling source
Linting source
Deploying stacks
dev-layer-chrome-aws-lambda-ExampleStack: deploying...
✅ dev-layer-chrome-aws-lambda-ExampleStack
Stack dev-layer-chrome-aws-lambda-ExampleStack
Status: deployed
Outputs:
ApiEndpoint: https://d9rxpfhft0.execute-api.us-east-1.amazonaws.com
Now if you head over to your API endpoint and add the URL and dimensions to the query string:
?url=https://sst.dev/examples&width=390&height=640
You should see Screenshot taken
being printed out.
Returning an image
Now let’s make a change to our function so that we return the screenshot directly as an image.
Replace the following lines in packages/functions/src/lambda.ts
.
// Take the screenshot
await page.screenshot();
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: "Screenshot taken",
};
with:
// Take the screenshot
const screenshot = (await page.screenshot({ encoding: "base64" })) as string;
return {
statusCode: 200,
// Return as binary data
isBase64Encoded: true,
headers: { "Content-Type": "image/png" },
body: screenshot,
};
Here we are returning the screenshot image as binary data in the body. We are also setting the isBase64Encoded
option to true
.
Now if you go back and load the same link in your browser, you should see the screenshot!
Deploying to prod
To wrap things up we’ll deploy our app to prod.
$ npx sst deploy --stage prod
This allows us to separate our environments, so when we are working in dev
, it doesn’t break the API for our users.
Cleaning up
Finally, you can remove the resources created in this example using the following commands.
$ npx sst remove
$ npx sst remove --stage prod
Conclusion
And that’s it! We’ve got a completely serverless screenshot taking API that automatically returns an image of any webpage we want. And we can test our changes locally before deploying to AWS! Check out the repo below for the code we used in this example. And leave a comment if you have any questions!
Example repo for reference
github.com/sst/sst/tree/master/examples/layer-chrome-aws-lambdaFor help and discussion
Comments on this exampleMore Examples
APIs
-
REST API
Building a simple REST API.
-
WebSocket API
Building a simple WebSocket API.
-
Go REST API
Building a REST API with Golang.
-
Custom Domains
Using a custom domain in an API.
Web Apps
-
React.js
Full-stack React app with a serverless API.
-
Next.js
Full-stack Next.js app with DynamoDB.
-
Vue.js
Full-stack Vue.js app with a serverless API.
-
Svelte
Full-stack SvelteKit app with a serverless API.
-
Gatsby
Full-stack Gatsby app with a serverless API.
-
Angular
Full-stack Angular app with a serverless API.
Mobile Apps
GraphQL
Databases
-
DynamoDB
Using DynamoDB in a serverless API.
-
MongoDB Atlas
Using MongoDB Atlas in a serverless API.
-
PostgreSQL
Using PostgreSQL and Aurora in a serverless API.
-
CRUD DynamoDB
Building a CRUD API with DynamoDB.
-
PlanetScale
Using PlanetScale in a serverless API.
Authentication
Using SST Auth
-
Facebook Auth
Adding Facebook auth to a full-stack serverless app.
-
Google Auth
Adding Google auth to a full-stack serverless app.
Using Cognito Identity Pools
-
Cognito IAM
Authenticating with Cognito User Pool and Identity Pool.
-
Facebook Auth
Authenticating a serverless API with Facebook.
-
Twitter Auth
Authenticating a serverless API with Twitter.
-
Auth0 IAM
Authenticating a serverless API with Auth0.
Using Cognito User Pools
-
Cognito JWT
Adding JWT authentication with Cognito.
-
Auth0 JWT
Adding JWT authentication with Auth0.
-
Google Auth
Authenticating a full-stack serverless app with Google.
-
GitHub Auth
Authenticating a full-stack serverless app with GitHub.
-
Facebook Auth
Authenticating a full-stack serverless app with Facebook.
Async Tasks
-
Cron
A simple serverless Cron job.
-
Queues
A simple queue system with SQS.
-
Pub/Sub
A simple pub/sub system with SNS.
-
Resize Images
Automatically resize images uploaded to S3.
-
Kinesis data streams
A simple Kinesis Data Stream system.
-
EventBus
A simple EventBridge system with EventBus.
Editors
-
Debug With VS Code
Using VS Code to debug serverless apps.
-
Debug With WebStorm
Using WebStorm to debug serverless apps.
-
Debug With IntelliJ
Using IntelliJ IDEA to debug serverless apps.
Monitoring
Miscellaneous
-
Middy Validator
Use Middy to validate API request and responses.