2
0

More than 3 years have passed since last update.

[serverless]AWS Lambdaでnode-canvasを利用した画像生成をする

Posted at

前書き

OGPの生成などnode.jsで画像生成をしたいというケースは最近増えてきているのでは無いでしょうか。その中でもクライアントで実行可能なCanvasを利用したいケースというのも同様に多いと思います。

またサーバーレスの台頭と共にデプロイにserverlessを利用しているケースもあると思います。

私自身がその様な環境で試す中、実行するまでに躓いた箇所があったので実際に動く簡単なアプリを作りながら途中の問題の解決法を解説していこうと思います。

実行環境

デプロイ: serverless(テンプレート: aws-nodejs-typescript)
実行環境: AWS Lambda (node.js v14)
ライブラリ: node-canvas
その他: GitHub Actions

完成形

AWS Lambdaで関数を実行するとSlacknode-canvasで生成した画像が投稿されるBotを作ります。

手順

1. serverlessテンプレートから雛形を生成

※ serverlessが導入されていなければ npm install -g serverless を実行

sls create -t aws-nodejs-typescript -p sls-node-canvas-test

大体このような構成で生成されます。

.
├── handler.ts
├── package.json
├── serverless.ts
├── tsconfig.json
└── webpack.config.js

2. テンプレートの不要部分削除とruntimeの更新

  • api-gateway 部分を削除
  • runtime を node.14 に変更

適用後の serverless.ts

import type { Serverless } from 'serverless/aws';

const serverlessConfiguration: Serverless = {
  service: {
    name: 'sls-node-canvas-test',
    // app and org for use with dashboard.serverless.com
    // app: your-app-name,
    // org: your-org-name,
  },
  frameworkVersion: '>=1.72.0',
  custom: {
    webpack: {
      webpackConfig: './webpack.config.js',
      includeModules: true
    }
  },
  // Add the serverless-webpack plugin
  plugins: ['serverless-webpack'],
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x',
  },
  functions: {
    hello: {
      handler: 'handler.hello'
    }
  }
}

module.exports = serverlessConfiguration;

3. 依存関係のインストール

npm install node-canvas @slack/web-api
npm install

4. コードの実装

handler.ts を以下のように実装します。

SlackのOAuth Access Tokenを取得して各自入れてください
必要な権限は chat:write chat:write.public files:write です

import 'source-map-support/register'
import { createCanvas } from 'canvas'
import { WebClient } from "@slack/web-api"

const token = "Your OAuth Access Token";

const web = new WebClient(token);

// 画像の生成
const createBufferImage = (): Buffer => {
  const canvas = createCanvas(100, 100)
  const ctx = canvas.getContext('2d')

  const text = "TEST"
  ctx.font = '30px'
  ctx.fillText(text, 50 - ctx.measureText(text).width / 2, 50)

  return Buffer.from(canvas.toDataURL().split(",")[1], "base64")
}

// slackにポスト 
const postSlack = async (image: Buffer) => {
  const res = await web.files.upload({
    file: image,
  });

  await web.chat.postMessage({
    channel: "general",
    text: res.file.permalink,
  });
}

export const hello = async () => {
  const image = createBufferImage()
  await postSlack(image)
}

5. ローカルで実行する

以下のコマンドでローカルで動いているか実行してみます。

sls invoke local -f hello

うまく行けば↓のように投稿されます。

スクリーンショット 2021-05-16 0.18.38.png

6. とりあえず、デプロイしてみる

私はMacで開発してますが、以下のコマンドでとりあえずデプロイしてみます。

sls deploy

デプロイできましたが、実行した所でエラーが発生しました。
スクリーンショット 2021-05-16 0.24.40.png

ネイティブコードを含んでいるため、Macでビルドした物はAWS Lambda上では動きませんでした。

7. GitHub Actions でデプロイする

node-gypのビルド処理がOSにより分かれるのでLinux系のOS上でビルドします。

Dockerでも良いですが、デプロイの自動化も同時にできるので今回は GitHub Actions を利用します。

.github/workflows/ci.yml に↓のファイルを作成する事で main ブランチにプッシュすると自動でデプロイされるようになります。

※ 事前にGitHubリポジトリの設定ページからsecretにAWSのキーを登録しておいてください

name: Deploy 

on:
  push:
    branches: [main]

defaults:
  run:
    shell: bash

jobs:
  deploy:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps: 
      - uses: actions/checkout@v2
      - name: Install dependencies & Deploy
        run:  |
          sudo apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
          npm ci
          npx serverless deploy --stage prod
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

デプロイされたので、実行してみます。今度は別のエラーが表示されました。
スクリーンショット 2021-05-16 1.03.21.png

8. ビルド時に共有ライブラリをコピーする

手順7のエラーでは必要なsoファイルが見つからないとなっているので、sls deploy で実行されるwebpackのビルド時に含めるようにします。

libuuid.so.1 が見つからないとなっていますが、実際に追加すると libmount.so.1libblkid.so.1 も見つからないと同様のエラーが表示されるので追加します。

soファイルはディストリビューション等により置いてる場所は違いますが、GitHub Actionsの ubuntu-latest (ubuntu-20.04) では必要なファイルは /usr/lib/x86_64-linux-gnu/ に存在しているのでコピーします。

webpack.config.js にコピープラグインを追加して以下のように書き換えます。

npm install copy-webpack-plugin@6 --save-dev
const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  context: __dirname,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: slsw.lib.webpack.isLocal ? 'cheap-module-eval-source-map' : 'source-map',
  resolve: {
    extensions: ['.mjs', '.json', '.ts'],
    symlinks: false,
    cacheWithContext: false,
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  target: 'node',
  externals: [nodeExternals()],
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      {
        test: /\.(tsx?)$/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
        options: {
          transpileOnly: true,
          experimentalWatchApi: true,
        },
      },
    ],
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: "/usr/lib/x86_64-linux-gnu/libuuid.so.1", to: "node_modules/canvas/build/Release/libuuid.so.1" },
        { from: "/usr/lib/x86_64-linux-gnu/libmount.so.1", to: "node_modules/canvas/build/Release/libmount.so.1" },
        { from: "/usr/lib/x86_64-linux-gnu/libblkid.so.1", to: "node_modules/canvas/build/Release/libblkid.so.1" },
      ],
    }),
  ],
};

これでデプロイして実行すると... 別のエラーが表示されます。

スクリーンショット 2021-05-16 3.59.28.png

9. node-canvasのバージョンを2.6.1にする

2021年5月16日現在、node-canvasの最新バージョンはv2.7.0で特にバージョンを指定せずに入れるとv2.7.0が入ると思いますが、手順8のエラーが解決できなかったので1つ前のv2.6.1を指定して入れ直します。

これでデプロイして実行し直すと実行には成功しますが、lambdaにはデフォルトフォントが存在しないので文字部分が豆腐で表示されています。

スクリーンショット 2021-05-16 4.14.33.png

スクリーンショット 2021-05-16 4.14.54.png

10. フォントを導入する

特に指定はありませんが、今回はNoto Sansを利用したいと思います。ダウンロードして NotoSans-Regular.ttfhandler.ts と同じ階層に配置します。

NotoSans-Regular.ttfregisterFont を使いフォントを読み込むように更新します。

import 'source-map-support/register'
import { createCanvas, registerFont } from 'canvas'
import { WebClient } from "@slack/web-api"
// @ts-ignore
import ttf from "./NotoSans-Regular.ttf"

registerFont(ttf, { family: "Noto Sans" })

const token = "Your OAuth Access Token";

const web = new WebClient(token);

// 画像の生成
const createBufferImage = (): Buffer => {
  const canvas = createCanvas(100, 100)
  const ctx = canvas.getContext('2d')

  const text = "TEST"
  ctx.font = '30px'
  ctx.fillText(text, 50 - ctx.measureText(text).width / 2, 50)

  return Buffer.from(canvas.toDataURL().split(",")[1], "base64")
}

// slackにポスト 
const postToSlack = async (image: Buffer) => {
  const res = await web.files.upload({
    file: image,
  });

  await web.chat.postMessage({
    channel: "general",
    text: res.file.permalink,
  });
}

export const hello = async () => {
  const image = createBufferImage()
  await postToSlack(image)
}

このままではwebpack でのビルド時にエラーが出るので file-loader を導入します。

npm install --save-dev file-loader

webpack.config.jsttf ファイルを file-loader で処理するように記述を追加します。

const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  context: __dirname,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: slsw.lib.webpack.isLocal ? 'cheap-module-eval-source-map' : 'source-map',
  resolve: {
    extensions: ['.mjs', '.json', '.ts'],
    symlinks: false,
    cacheWithContext: false,
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  target: 'node',
  externals: [nodeExternals()],
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      {
        test: /\.(tsx?)$/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
        options: {
          transpileOnly: true,
          experimentalWatchApi: true,
        },
      },
      {
        test: /\.ttf$/i,
        loader: 'file-loader',
      },
    ],
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: "/usr/lib/x86_64-linux-gnu/libuuid.so.1", to: "node_modules/canvas/build/Release/libuuid.so.1" },
        { from: "/usr/lib/x86_64-linux-gnu/libmount.so.1", to: "node_modules/canvas/build/Release/libmount.so.1" },
        { from: "/usr/lib/x86_64-linux-gnu/libblkid.so.1", to: "node_modules/canvas/build/Release/libblkid.so.1" },
      ],
    }),
  ],
};


デプロイして、実行...

スクリーンショット 2021-05-16 10.37.09.png

やっとできました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0