In this example we will look at how to use Layers in your serverless app with Serverless Stack (SST). We’ll be using the chrome-aws-lambda 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

Create an SST app

Let’s start by creating an SST app.

$ npx create-serverless-stack@latest layer-chrome-aws-lambda
$ cd layer-chrome-aws-lambda

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.json in your project root.

{
  "name": "layer-chrome-aws-lambda",
  "stage": "dev",
  "region": "us-east-1"
}

Project layout

An SST app is made up of two parts.

  1. lib/ — App Infrastructure

    The code that describes the infrastructure of your serverless app is placed in the lib/ directory of your project. SST uses AWS CDK, to create the infrastructure.

  2. src/ — App Code

    The code that’s run when your API is invoked is placed in the src/ directory of your project.

Creating the API

Let’s start by creating our API.

Replace the lib/MyStack.js with the following.

import { LayerVersion } from "@aws-cdk/aws-lambda";
import * as sst from "@serverless-stack/resources";

const layerArn =
  "arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:22";

export default class MyStack extends sst.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);

    const layer = LayerVersion.fromLayerVersionArn(this, "Layer", layerArn);

    // Create a HTTP API
    const api = new sst.Api(this, "Api", {
      routes: {
        "GET /": {
          function: {
            handler: "src/lambda.handler",
            // Increase the timeout for generating screenshots
            timeout: 15,
            // Load Chrome in a Layer
            layers: [layer],
            // Exclude bundling it in the Lambda function
            bundle: { externalModules: ["chrome-aws-lambda"] },
          },
        },
      },
    });

    // Show the endpoint in the output
    this.addOutputs({
      ApiEndpoint: api.url,
    });
  }
}

Here, we are first getting a reference to the ARN of the Layer we want to use. Head over to the chrome-aws-lambda Layer repo and grab the one for your region.

We then use the sst.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 chrome-aws-lambda npm package.

Finally, we output the endpoint of our newly created API.

Let’s install the npm package we are using here.

From the project root run the following.

$ npx sst add-cdk @aws-cdk/aws-lambda

The reason we are using the add-cdk command instead of using an npm install, is because of a known issue with AWS CDK. Using mismatched versions of CDK packages can cause some unexpected problems down the road. The sst add-cdk command ensures that we install the right version of the package.

Adding function code

Now in our function, we’ll be handling taking a screenshot of a given webpage.

Replace src/lambda.js with the following.

import chrome from "chrome-aws-lambda";

// chrome-aws-lambda handles loading locally vs from the Layer
const puppeteer = chrome.puppeteer;

export async function handler(event) {
  // Get the url and dimensions from the query string
  const { url, width, height } = event.queryStringParameters;

  const browser = await puppeteer.launch({
    args: chrome.args,
    executablePath: await chrome.executablePath,
  });

  const page = await browser.newPage();

  await page.setViewport({
    width: Number(width),
    height: Number(height),
  });

  // Navigate to the url
  await page.goto(url);

  // Take the screenshot
  await page.screenshot();

  return {
    statusCode: 200,
    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 this from the root.

$ npm install puppeteer puppeteer-core chrome-aws-lambda

The puppeteer packages are used internally by the chrome-aws-lambda package.

Starting your dev environment

SST features a Live Lambda Development environment that allows you to work on your serverless apps live.

$ npx sst start

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-my-stack: deploying...

 ✅  dev-layer-chrome-aws-lambda-my-stack


Stack dev-layer-chrome-aws-lambda-my-stack
  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://serverless-stack.com/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 src/lambda.js.

  // Take the screenshot
  await page.screenshot();

  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    body: "Screenshot taken"
  };

with:

  return {
    statusCode: 200,
    // Return as binary data
    isBase64Encoded: true,
    headers: { "Content-Type": "image/png" },
    body: await page.screenshot({ encoding: "base64" }),
  };

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!

Chrome screenshot in Lambda function

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!