Automattic / node-canvas

Node canvas is a Cairo backed Canvas implementation for NodeJS.
9.98k stars 1.15k forks source link

AWS Lambda: cannot find canvas.node #1920

Open michaeldclifford opened 2 years ago

michaeldclifford commented 2 years ago

Issue

Attempting to use Canvas on Lambda results in the following error:

/var/task/node_modules/canvas/build/Release/canvas.node:
cannot open shared object file: No such file or directory

Context

I'm attempting to use chartjs-node-canvas on Lambda.

The Installation Guide says that Canvas 2 should work, so long as it is installed on a Linux docker container, so I have used the following Dockerfile

FROM amazonlinux:2
RUN yum update -y && yum group install "Development Tools" -y
RUN curl -fsSL https://rpm.nodesource.com/setup_14.x | bash - && yum install -y nodejs
RUN yum install cairo-devel libjpeg-turbo-devel giflib-devel pango-devel -y

to set up a container, and then run npm install inside the container, before packaging up the node_modules and output folders into one .zip file.

Unzipping the file locally shows that canvas.node exists at /node_modules/canvas/build/Release/canvas.node, but the error mentioned above occurs when running the lambda.

Things I've tried that (sadly) haven't worked (yet)

Steps to Reproduce

Run the following as part of a Lambda function

const canvas = new ChartJSNodeCanvas({ width: 3840, height: 2160 });
const image = canvas.renderToBufferSync(data);

Your Environment

flohall commented 2 years ago

Try this - works with canvas@2.9.0 on arm64 and amd64. It also contains ffmpeg a powerful tool to render multiple frames into a video. But if don't work with video delete ffmepg again, as need quite a lot of time to build. Also get rid of the additional fonts as they double the size of the container, if you don't need them.

I have developed this container build in one of our last projects.

### BUILDER CONTAINER IMAGE only used for building
FROM public.ecr.aws/lambda/nodejs:14 as builder

# update (and installing of software without fixed version) means that without code changes behaviour could be slightly different if a new image is created
# therefore every image version should be tested on staging system and deployed as is to production later
# this way we get most important OS security fixes
RUN yum -y update

# only needed for setup (execution of subsequent command "tar")
RUN yum -y install tar xz

# ffmpeg build from source: https://trac.ffmpeg.org/wiki/CompilationGuide/Centos
## ffmpeg build tools and libs like specified in installation guide
RUN yum -y install autoconf automake bzip2 bzip2-devel cmake freetype-devel gcc gcc-c++ git libtool make pkgconfig zlib-devel
RUN mkdir /opt/ffmpeg_sources
WORKDIR /opt/ffmpeg_sources

## ffmepg nasm assembler
RUN curl -O -L https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/nasm-2.15.05.tar.bz2
RUN tar xjvf nasm-*.tar.bz2
RUN rm nasm-*.tar.bz2
RUN cd nasm-* && ./autogen.sh
RUN cd nasm-* && ./configure --prefix="/opt/ffmpeg_build" --bindir="/opt/bin"
RUN cd nasm-* && make
RUN cd nasm-* && make install
## ffmpeg yasm assembler
RUN curl -O -L https://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
RUN tar xzvf yasm-*.tar.gz
RUN rm yasm-*.tar.gz
RUN cd yasm-* && ./configure --prefix="/opt/ffmpeg_build" --bindir="/opt/bin"
RUN cd yasm-* && make
RUN cd yasm-* && make install
## ffmpeg libx264 video codec
RUN git clone --branch stable --depth 1 https://code.videolan.org/videolan/x264.git
RUN cd x264 && PKG_CONFIG_PATH="/opt/ffmpeg_build/lib/pkgconfig" ./configure --prefix="/opt/ffmpeg_build" --bindir="/opt/bin" --enable-static
RUN cd x264 && make
RUN cd x264 && make install
## ffmepg mp3 audio codec
RUN curl -O -L https://downloads.sourceforge.net/project/lame/lame/3.100/lame-3.100.tar.gz
RUN tar xzvf lame-*.tar.gz
RUN rm lame-*.tar.gz
RUN cd lame-* && ./configure --prefix="/opt/ffmpeg_build" --bindir="/opt/bin" --disable-shared --enable-nasm
RUN cd lame-* && make
RUN cd lame-* && make install
## ffmpeg build
RUN curl -O -L https://ffmpeg.org/releases/ffmpeg-5.0.tar.bz2
RUN tar xjvf ffmpeg-*.tar.bz2
RUN rm ffmpeg-*.tar.bz2
RUN cd ffmpeg-* && PKG_CONFIG_PATH="/opt/ffmpeg_build/lib/pkgconfig" ./configure \
                   --prefix="/opt/ffmpeg_build" \
                   --pkg-config-flags="--static" \
                   --extra-cflags="-I/opt/ffmpeg_build/include" \
                   --extra-ldflags="-L/opt/ffmpeg_build/lib" \
                   --extra-libs=-lpthread \
                   --extra-libs=-lm \
                   --bindir="/opt/bin" \
                   --enable-gpl \
                   --enable-libfreetype \
                   --enable-libmp3lame \
                   --enable-libx264 \
                   --enable-nonfree
RUN cd ffmpeg-* && make
RUN cd ffmpeg-* && make install

WORKDIR /var/task

# build tools needed for installation of npm package "canvas"
RUN yum -y install gcc-c++ make

# libraries needed for installation of npm package "canvas"
RUN yum -y install cairo-devel pango-devel libjpeg-turbo-devel giflib-devel librsvg2-devel bzip2-devel

# copy only files needed for npm install - so npm install will only be executed again if those files have changed
# .npmrc is needed for the vm-rendering-library to authenticate against nexus
COPY package.json .npmrc ./

# install all npm dependencies including dev dependencies
RUN npm install

# copy all remaining files not excluded in .dockerignore
COPY ./ ./

# build typescript
RUN npm run build

# rename app.js to app.mjs to call ESM module from commonJS
RUN find ./dist -type f -name "*.js" -and -not -name "handler.js" -exec sh -c 'mv "$0" "${0%.js}.mjs"' {} \;

# remove npm dev dependencies as they aren't needed anymore - make node_modules production ready
# RUN npm prune --production

### END OF BUILDER CONTAINER IMAGE

### START OF RUNTIME CONTAINER IMAGE
FROM public.ecr.aws/lambda/nodejs:14

# install again because some dependencies are needed at runtime
RUN yum -y install cairo-devel pango-devel libjpeg-turbo-devel giflib-devel librsvg2-devel bzip2-devel

# Non latin characters must be installed
RUN yum -y install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
RUN yum -y install kde-l10n-Chinese
RUN yum -y install kde-l10n-Japanese
RUN yum -y install kde-l10n-Arabic
RUN yum -y install kde-l10n-Korean
RUN yum -y reinstall glibc-common
RUN yum -y install kde-l10n-Chinese.noarch google-noto-sans-traditional-chinese-fonts.noarch google-noto-sans-simplified-chinese-fonts.noarch
RUN yum -y install kde-l10n-Japanese.noarch google-noto-sans-japanese-fonts.noarch
RUN yum -y install kde-l10n-Korean.noarch google-noto-sans-korean-fonts.noarch
RUN yum -y install kde-l10n-Arabic.noarch google-noto-sans-arabic-fonts.noarch

# copy over ffmepg from builder
COPY --from=builder /opt/ffmpeg_sources/ffmpeg-*/ffmpeg /opt/ffmpeglib/ffmpeg
# copy over node_modules from builder
COPY --from=builder /var/task/node_modules ./node_modules

# is set automatically by the docker platform attribute (cpu-arch) - this dockerfile will only work with "*/amd64" or "*/arm64"
ARG TARGETARCH

# manual installation for lambda insights extension, as this seems to be the only way for arm64 (at least right now, arm64 on AWS Lambda is pretty new)
COPY --from=builder /var/task/lambda-insights-extension/${TARGETARCH}/  /opt/
# make lambda insights extension executable
RUN chmod a+x "/opt/extensions/cloudwatch_lambda_agent"

ARG SERVICE_NAME
# copy over only the files from that specific service - rename the folder to service, as dynamic command names in docker are difficult
COPY --from=builder /var/task/dist/${SERVICE_NAME} ./dist/service

# fixing an issue in npm package canvas - must be executed after npm install
ENV LD_PRELOAD=/var/task/node_modules/canvas/build/Release/libz.so.1

ENV NODE_OPTIONS="--experimental-specifier-resolution=node"

# enable the lambda being executed - the correct handler function should be called
CMD ["dist/service/src/lambda/handler.handler"]