Chart.jsのグラフを画像ファイルに出力するAWS Lambda関数をコンテナイメージで構築する

## はじめに
こんにちは。READYFORでプロダクトエンジニアをやっておりますtoyocです。
この記事は「[READYFOR Advent Calendar 2022](https://qiita.com/advent-calendar/2022/readyfor)」の7日目の記事です。

## 概要
AWS Lambda x [Chart.js](https://www.chartjs.org/) で画像ファイル出力する際に、コンテナイメージベースでデプロイすることで、めちゃくちゃめんどくさかった構築が、まぁまぁめんどくさいぐらいで済むようになったというお話です。
Chart.jsでグラフを書いたり、実際に画像出力する部分のコードは割愛します。

## あらすじ
> 「いい感じのグラフをこのページに作ってほしい！」
「じゃあ[Chart.js](https://www.chartjs.org/)でいい感じのを作ろう。…ほいできた。」
「いいね！ページに載せるのはこれでいいんだけど、ページのサムネとかにも使いたいからこのグラフ画像化してくれる？」
「毎日データが変わるページだから毎日画像化しないとだよね？」
「そうだね。」
「今だと、ブラウザ上のJSで動的にCanvasに描画してるから、なかなか難しいんだけど…？」
「そっかー、まぁよくわからないけど、よろしく！」
（※弊社ではこんな投げっぱなしなタスクの振られ方は基本的にありません）

こんな風にChart.jsで生成したグラフを画像に変換しなければいけなくなることってありますよね？
そう言った時にサーバー上でChart.jsの描画結果を画像ファイルとして出力できるライブラリとして、[chartjs-node-canvas](https://www.npmjs.com/package/chartjs-node-canvas)を使います。
ただ、このライブラリは[node-canvas](https://www.npmjs.com/package/canvas)というCanvas APIを[cairo](https://www.cairographics.org/)で再実装したライブラリを利用しております。
このcairoは環境に合わせてビルドする必要があり、AWS Lambdaで動かしたいなーと思った場合、Amazon Linux2でビルドしたcairoを使う必要があります。

## zipファイルデプロイの場合
詳細な手順は [node-canvasのドキュメント](https://github.com/Automattic/node-canvas/wiki/Installation:-AWS-Lambda) や [参考記事](https://note.com/daharas/n/nc1f37ad3ba0e)をみていただきたいのですが、
手順をかいつまんで説明すると
1. Amazon Linux2をEC2なり好きなDocker環境なりで立てる
1. その中で必要なライブラリとnode-canvasをインストールする（↓は[こちら](https://github.com/Automattic/node-canvas/wiki/Installation:-AWS-Lambda)から拝借）
    ```sh
    sudo yum install cairo-devel libjpeg-turbo-devel giflib-devel pango-devel -y
    npm install canvas@next
    ```
1. `/usr/lib64/`配下にある**必要なライブラリ**（後述）と`node_modules`をローカルにコピーする
1. Lambda関数を構築するディレクトリ内に↑を配置し、必要なフォントファイルやChart.jsで描画するhandlerスクリプトなどを配置します。（↓は[こちら](https://note.com/daharas/n/nc1f37ad3ba0e)から拝借したディレクトリ構成）
    ```
    LambdaFunction
    ┣lib
    ┣node_modules
    ┣index.js
    ┣ipaexg.ttf
    ┗package-lock.json
    ```
1. zip圧縮してLambda関数としてアップロードして完了です。

### 必要なライブラリについて
↑の3で `/usr/lib64/`配下のライブラリをコピーすると書きましたが、何が必要なのかが`node-canvas`や`node`のバージョンによって異なります。今までにあげた公式ドキュメントや参考記事でも異なりますし、後述する僕が構築する際に必要だったライブラリも異なります。
なので、文字通りトライアンドエラーで、実行時に足りないと言われたライブラリを手探りでコピーしていく作業が必要になります。
zipファイルデプロイの場合、サーバーを立ててそこからコピーして、というやりかたになるので大変煩雑です。
Dockerイメージを使ったコンテナデプロイにすることで、この煩雑さを（若干）軽減してこうと思います。

## コンテナデプロイの場合
1. DockerFileを書きます
    ```Dockerfile
    FROM public.ecr.aws/lambda/nodejs:16

    # carioに必要なライブラリをインストールします
    RUN yum -y install cairo-devel pango-devel libjpeg-turbo-devel giflib-devel gdk-pixbuf2 gcc-c++
    # Lambda実行時に読み込まれるディレクトリを作ります
    RUN mkdir ${LAMBDA_TASK_ROOT}/lib
    # 必要なライブラリ（今回は結局1ファイルだけで済みました）をコピーします
    RUN cp /usr/lib64/libgdk_pixbuf-2.0.so.0 ${LAMBDA_TASK_ROOT}/lib
    # chartjs-node-canvasなどインストールします
    COPY package.json ${LAMBDA_TASK_ROOT}/
    RUN npm install
    # Lambda実行時に優先してライブラリが読み込まれるように環境変数を設定しておきます
    ENV LD_PRELOAD ${LAMBDA_TASK_ROOT}/node_modules/canvas/build/Release/libz.so.1

    # その他グラフ出力を実際に行うためのファイルをコピーします
    COPY *.js ${LAMBDA_TASK_ROOT}
    COPY ipaexg.ttf ${LAMBDA_TASK_ROOT}

    WORKDIR ${LAMBDA_TASK_ROOT}

    # グラフ出力する関数を指定します
    CMD [ "handler.iikanjino_graph_kakidasu" ]
    ```
2. コンテナデプロイします（[公式ドキュメントの手順](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-images.html)）

（色々と端折ったので）手順の数はかなり少なくなりました。ヤッタネ！
とはいえ、このDockerFileを書くまでにそれなりにトライアンドエラーがありました。

### 実行エラーへの対応
構築の際、主に、必要なライブラリが足りないことによるエラーが発生します。
zipファイルデプロイの場合は、サーバーを立てて、そこからライブラリをコピーしてきて、実際に動かしてみてエラーが出たらコピーするものを変えてみる。というトライアンドエラーが必要でした。また、Lambdaのエラー出力しか手がかりがないので試行回数がどうしても多くなってしまいます。
一方、コンテナデプロイの場合は、最低限のDockerFileを一度書いてからマウントしてそのなかで動かしてみてエラーに対応することができます。マウントした中で環境変数やインストール済みのライブラリを確認したりできるので、エラーの手がかりも得やすかったです。

## まとめ
ということで、コンテナイメージを使うことで、zipファイルデプロイよりお手軽に構築できたという話でした。
今回はChart.js (node-canvas)でしたが、他にもこういう例はありそうですね。

明日12月8日は小野さん（@ono_04）担当です。お楽しみに！