Running headful Chrome with extensions in a Lambda function

G
5 min readJan 17, 2021

--

As a Serverless enthusiast, I tend to use Lambda for EVERYTHING. I find the combination of dev speed, scalability and integration with other AWS services irresistible and it’s served me well over the last 5 years.

However, I never managed to run a fully fledged Chrome browser (with extensions) in a lambda function. Until now.

The Challenge

There are a couple issues with this use case. First, lambda by default is not made to run graphical applications. On top of that, most packages I found were either outdated (node 6.10 anyone??), and/or outright didn’t run.

After a lot of unsuccessful tests, I settled for chrome-aws-lambda which seemed to be up to date, maintained, and working.

Hurdle #1: Getting chrome to run :

After a bit of tinkering, and a lot of googling, here are the flags I found necessary to run chrome-aws-lambda :

const chromeFlags = ['--no-xshm','--disable-dev-shm-usage','--single-process','--no-sandbox','--no-first-run',`--load-extension=${extensionDir}`]

In short, it’s all about accomodating the non-standard environment and filesystem we’re running on. Look here for full details: https://peter.sh/experiments/chromium-command-line-switches/

With those, I was able to run the following function :

Given that I was running without the --headless switch, I was pretty confident that I had cracked it. After a bit of cleanup and packaging all the heavy dependencies into a layer, I set out to work on talking to my extension.

Hurdle #2: chrome.runtime :

To work with extensions, we need the chrome.runtimeAPI. https://developer.chrome.com/docs/extensions/reference/runtime/

However, when running the function, the following happened :

const runtimeAPI = await Runtime.evaluate({ 
expression: 'chrome'
})
The chrome object doesn’t exit

In my DevTools, the chrome object was definitely there. Further research led me to this very informative and disappointing post on the DevTools Github.

Given that we don’t intend to support extensions in headless mode and there aren’t that many other useful things behind the chrome object, I don’t think implementing it is a very high priority.

copyright Disney/Pixar

Enter Container Images

When I read the news at re:Invent 2020 about container images, I felt like it was worth giving it a spin. In particular, this bit sounded very interesting :

Lambda provides open-source runtime interface clients that you add to an alternative base image to make it compatible with Lambda.

The promise of being able to slap a Lambda runtime onto any docker image seemed almost too good to be true, and I was expecting to fail miserably (but learn stuff in the process). I needed to know.

The idea

I figured I’d need the following :

If Chrome was running in the container, chrome-launcher would find it and the lambda itself would stay 99% the same.

Running Chrome in Docker

I followed this amazing guide by Stephen Fox, updated ubuntu to focal, and added gnupg2 after an explicit error message.

In no time, I was able to VNC into my Docker container and launch Chrome. So far so good.

Adding the Lambda RIC

You can find the aws-lambda-ric for Node here : https://github.com/aws/aws-lambda-nodejs-runtime-interface-client

The AWS guys have done a decent job at documenting the package and its use, it boils down to adding the following to the docker container:

Adding the emulator

On top of the RIC, AWS provides us with a convenient Lambda Runtime Interface Emulator (RIE) to let us test our container image locally. It’s available on Github, and can be added to the Dockerfile like this:

The entrypoint.sh script is a simple if condition that checks if we’re running on lambda or locally and uses the emulator in the latter case.

Modifying bootstrap script for Lambda

I made a few changes to suit the task at hand :

  • Removed the VNC server
  • Added the emulator entrypoint

Click this for the full bootstrap file

Adding the function code

All that was left to do was adding the function code and pointing it to the app handler.

The mods to the function itself were minimal, I removed the chrome-aws-lambda package and changed the flags to:

export const chromeFlags = ['--no-first-run','--window-size=1366,768',`--load-extension=${extensionDir}`]

Build and run

The build step took a little long the first time (10+ minutes), notably the ric install. Fortunately Docker caches everything and the pain was short lived

docker build -t chrometest:latest . 

After that, I used the following to run the container:

docker run -p 9000:8080 --user apps --privileged chrometest:latest

And all that was left was to test. In a different terminal, I ran:

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
container log

Seeing the familiar Lambda START - END logs in my container felt very satisfying, but the result of the function itself made me jump up and down like a madman:

FINALLY! The chrome object is defined, we have a fully fledged “headful” chrome browser, on a lambda runtime, able to use the chrome.runtime API.

copyright Disney/Pixar

All that’s left to do is dump the container on AWS, and voila!

Ship to AWS

To use the container in your lambda functions, just deploy it to the ECR registry as described here

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.comdocker tag  chrometest:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/chrometest:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/chrometest:latest

Would I use again?

Frankly, I don’t think I’ll have that many use cases for Lambda Container Images. That being said, it is a great addon to AWS suite and fills a gap in the Serverless ecosystem that was missing for years. I wasn’t expecting it to be that painless, the whole thing took a few hours, and it “just worked”.

Full project code on Github

--

--