The Cairo-backed node-canvas has been a pillar for many Node.js based projects that need to simulate a browser canvas on the server-side. One of our recent projects uses Apache ECharts to render some graphs. We have successfully shifted the rendering part of ECharts to the server side to ease the burden on the front-end Web browsers using node-canvas
and jsdom.
Thing goes well until we decided to migrate the rendering task to AWS Lambda Node.js 14.x. Unfortunately, AWS Lambda runtime environment is quite slim and hence, insufficient for node-canvas
. There are many issues reported on the project Github page. In summary, the root cause is that node-canvas
must be built with native libraries when we run npm install
. Unless you are running Amazon Linux 2, which is the native runtime environment for AWS Lambda functions, there will be discrepancy between your local environment with AWS Lambda runtime environment. The typical error message is Error: libXXX: cannot open shared object file: No such file or directory
. I’m developing on a Ubuntu 20 LTS box but still got many of those errors.
After giving exhaustive trials and errors adding missing libraries, there is a final and ultimate error regarding the libz
. Adding libz
even with the exact version 1.2.9 into the Lambda layer does not make the error go away. I came across a post by Max Duncan and realised I haven’t thought about the other option with AWS Lambda, i.e. creating a container image. This approach has been introduced recently, around Dec 2020.
Using a similar approach, I can successfully build an AWS Node.js 14.x container for node-canvas
for ECharts. Nevertheless, the same approach can be used to build working node-canvas containers for any other libraries as well.
Create a simple project as following
.
├── Dockerfile
├── index.js
└── package.json
The file index.js
will be the entry point with the handler.
'use strict';
exports.handler = async (event, context, callback) => {
const result = {
body: null,
isBase64Encoded: false,
statusCode: 200
};
console.log('Received a render request event');
try {
result.body = ... ; // call another function using Node Canvas
} catch (error) {
console.log(error);
result.statusCode = 500;
result.body = JSON.stringify(error);
return callback(result);
}
return callback(null, result);
};
The package.json
is a simple NPM specification with the dependency to canvas
, for instance
{
"name": "node-canvas-lambda-container",
"version": "1.0.0",
"dependencies": {
"canvas": "^2.8.0"
},
"license": "MIT"
}
The Dockerfile
is the Docker container specification in which we create a new container image based on AWS public Node.js 14 image.
FROM public.ecr.aws/lambda/nodejs:14
##
# Install necessary package for building Node.js Canvas
##
RUN yum -y update \
&& yum -y groupinstall "Development Tools" \
&& yum install -y nodejs gcc-c++ cairo-devel \
libjpeg-turbo-devel pango-devel giflib-devel \
zlib-devel librsvg2-devel
COPY *.js package* ./
RUN npm install
ENV LD_PRELOAD=/var/task/node_modules/canvas/build/Release/libz.so.1
RUN yum remove -y cairo-devel libjpeg-turbo-devel \
pango-devel giflib-devel zlib-devel librsvg2-devel
# Set the CMD to your handler
CMD [ "index.handler" ]
Now we can build the container
docker build -t node-canvas-lambda .
If the build is successful, we can see the new Docker image in our local environment
$ docker images | grep node-canvas
node-canvas-lambda latest ae9f63e1981a 11 seconds ago 1.72GB
Start the container for testing (no need to deploy to AWS yet)
$ docker run -p 3000:8080 node-canvas-lambda
time="2021-10-19T01:27:46.528" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"
Per AWS documentation, the container includes the AWS runtime interface clients needed to run. We can test the Lambda locally with curl
or similar tools. Note that, the port has to be exact Docker port mapping 3000:8080
. The rest of the URL has to be 2015-03-31/functions/function/invocations
.
Open another teminal (as we need to keep the Docker Node.js container running) and use the following command:
$ curl -X POST "http://localhost:3000/2015-03-31/functions/function/invocations" \
--data '{}'
{"body":null,"isBase64Encoded":false,"statusCode":200}
If thing goes well, we should see some JSON outputs from the Lambda handler (i.e index.handler
).