Lambda Layer で ImageMagick をインストールする

Lambda の `Node.js 8.x` イメージには ImageMagick とそれを Node.js から使うライブラリがなぜかプレインストールされていました。ですが`Node.js 10.x` 系以降から削除されてしまったので、Lambda Layer でImage Magick をインストールする方法と後学のために実行ファイルをビルドし Lambda に配置する方法をご紹介します
2020.01.04

西田@大阪です

Lambda の Node.js 8.x イメージには ImageMagick とそれを Node.js から使うライブラリがなぜかプレインストールされていました。ですがNode.js 10.x 系以降から削除されてしまったので、Lambda Layer でImage Magick をインストールする方法と後学のために実行ファイルをビルドし Lambda に配置する方法をご紹介します

結論から

serverlesspub/imagemagick-aws-lambda-2: ImageMagick for AWS Lambda 2 runtimes

これを使えばOKです

後学のために、これにたどり着くまでにいろいろ試行錯誤した結果を残しておきたいと思います

やりたいこと

Node.js 8.xのイメージに入っていたnpm rsms/node-imagemagick こちらを使ってS3にアップロードされたファイルをリサイズできるようにしてみたいと思います

まずはソースを読んで必要なファイルを確認します。(よくみるとしばらく更新されていないライブラリですね。。。)

リサイズする時に使用するexportsされたresizeメソッドを確認します

exports.resize = function(options, callback) {
  var t = exports.resizeArgs(options);
  return resizeCall(t, callback)
}

271行目

ここからたどっていくと convert というメソッドが呼ばれている事がわかります。convertメソッドの中では"convert"という文字列とそのパラメーターをexec2という関数に渡して呼び出していることがわかります

exports.convert = function(args, timeout, callback) {
  // 〜〜〜省略 
  return exec2(exports.convert.path, args, procopt, callback);
}
exports.convert.path = 'convert';

243行目

exec2では child_process.spawnを使って引数で渡された文字列(今回は convert)をコマンドとして実行していることがわかります

var childproc = require('child_process')
// 〜〜〜省略 

function exec2(file, args /*, options, callback */) {
  // 〜〜〜省略 
  var child = childproc.spawn(file, args);
  // 〜〜〜省略 
}

5行目

そのため convertという実行ファイルを PATHが通っているところにおいてあげれば動きそうです。※ convertはImageMagickをインストールすれば一緒にインストールされるコマンドの一つです

convert実行ファイルをLambda環境に配置し、Node.jsから使えるようにするには以下の作業が必要です

  1. convert実行ファイルをLinux環境でビルドする
  2. convert実行ファイルをPATH配下のディレクトリに配置するLambda Layer を作成し、デプロイするLambdaから参照する
  3. imagemagicknpmをLambda環境にインストールする
  4. imagemagicknpmを利用しリサイズする Lambda をデプロイする

1 は筆者は MacOS を利用しているので、EC2もしくはDockerを使う方法がありますが、後者のDockerを使う方法でビルドしたいと思います

2〜4 は Serverless Framework(以下 sls)を使って行いたいとおもいます

Lambda環境のPATHが通ってるとこに実行ファイルをおくには

Linuxでは実行ファイルを途中のパスを省略しファイル名だけで実行するにはPATH環境で指定されているパス以下に実行ファイルを配置する必要があります

また、実行ファイルから共有ライブラリが動的にリンクされている場合にはLD_LIBRARY_PATH環境変数で指定されているパス以下にsoファイルを配置する必要があります

LambdaのPATH環境変数や、LD_LIBRARY_PATH環境変数を確認するには以下をご参考ください

参考: Lambda 関数で使用できる環境変数 - AWS Lambda

今回は Lambda Layer がファイルを /opt以下に配置するので、/opt以下で環境変数が通ってるところ /opt/bin(PATH)、/opt/lib(LD_LIBRARY_PATH)にそれぞれのファイルを配置していきたいと思います

前提条件

すでにS3のバケットが作成されている前提での説明とさせていただきます

Serverless Frameworkでプロジェクト作成

sls create --template aws-nodejs --path imagemagicksample

作成された serverless.yml

provider:
  name: aws
  runtime: nodejs12.x

執筆時点で上記コマンド作成されたプロジェクトの Runtime が nodejs12.x だったので、このままこのプロジェクト上に ImageMagickに必要なファイルを作成していきます

作業用ディレクトリを作成します

mkdir -p layer/imagemagick/lib
mkdir -p layer/imagemagick/bin
mkdir -p volume/work

ここまででディレクトリは以下のようになっています

├── handler.js
├── serverless.yml
├── layer # Lambda Layer に含めるファイルを配置するディレクトリ
│   └── imagemagick
│       ├── bin
│       └── lib
└── volume # Dockerにマウントする作業用ディレクトリ

Docker を使って必要なファイルをビルド

Lambdaのほぼ同様の構成の Docker Image である lambci/docker-lambda を使って必要なファイルをビルドしていきます

必要なファイルをダウンロード

volume/work以下にビルドに必要なファイルをダウンロードしていきます

host> cd volume/work

ImageMagickに必要なファイルをここからダウンロードしてきます。以下は執筆時点の最新のバージョンの tar.gzをダウンロードするコマンドです

host> wget https://imagemagick.org/download/ImageMagick-7.0.9-13.tar.gz

今回は JPEG に対応するのでここからJPEGの拡張jpegsr*もダウンロードします

host> wget http://www.imagemagick.org/download/delegates/jpegsrc.v9b.tar.gz

JPEG拡張の共有ライブラリをビルドする

volumeディレクトリを/optにマウントしDockerを起動します。※ ローカルにDockerイメージのキャッシュがなければダウンロードが始まることにご注意ください

host> cd ../..
host> docker run -it --rm -v $PWD:/opt lambci/lambda-base-2:build /bin/bash

docker内の /opt/work ディレクトリに移動し、必要なファイルを解凍します

docker> cd /opt/work
docker> tar xfz jpegsrc.v9b.tar.gz

JPEG拡張をコンパイルします。configure--prefixパラメーターに/opt/を指定することで、/opt/lib以下に共有ライブラリが配置されるようにしています

docker> cd jpeg-9b
docker> ./configure \
  CPPFLAGS=-I/opt/include \
  LDFLAGS=-L/opt/lib \
  --prefix=/opt/ \
  --disable-dependency-tracking \
  --enable-shared
docker> make && make install

ヘッダファイルがインストールされていることを確認します

docker> ls /opt/include/
jconfig.h  jerror.h  jmorecfg.h  jpeglib.h

共有ライブラリがインストールされていることを確認します

docker> ls /opt/lib/
libjpeg.a  libjpeg.la  libjpeg.so  libjpeg.so.9  libjpeg.so.9.2.0

ImageMagick本体をビルドする

docker内の /opt/work ディレクトリに移動し、必要なファイルを解凍します

docker> cd /opt/work
docker> tar xfz ImageMagick-7.0.9-13.tar.gz

ImageMagickをコンパイルします。configure--prefix/optパラメーターを指定しています。

CPPFLAGSLDFLAGSパラメーターに/opt以下のパスを渡しJPEG拡張のヘッダファイル、共有ライブラリをインストールした場所を指定しています。

また、今回使用しない機能を無効化しています

docker> cd ImageMagick-7.0.9-13 
docker> ./configure \
  CPPFLAGS=-I/opt/include \
  LDFLAGS=-L/opt/lib \
  --prefix=/opt/ \
  --disable-docs \
  --without-modules \
  --enable-delegate-build \
  --disable-dependency-tracking \
  --without-magick-plus-plus \
  --without-perl \
  --without-x \
  --disable-openmp \
  --disable-dependency-tracking

docker> make && make install

コマンドの動作を確認します。※ sample.jpeg は適当な画像ファイルをダウンロードしてくる想定です

/opt/bin/convert -resize 100x100 sample.jpeg resized.jpeg

ImageMagickコマンドに必要なファイルを確認する

必要なファイルを確認します

docker> ls -l /opt/bin/convert 
lrwxrwxrwx 1 root root 6 Dec 31 20:15 /opt/bin/convert -> magick

lsコマンドで確認するとconvert実行ファイルは同ディレクトリにあるmagick実行ファイルのシンボリックリンクのようなので、この2つのファイルは必要と思われます

magickコマンドの共有ライブラリの依存関係を確認します

docker> ldd /opt/bin/magick
linux-vdso.so.1 (0x00007ffc0aff0000)
libMagickCore-7.Q16HDRI.so.7 => /opt/lib/libMagickCore-7.Q16HDRI.so.7 (0x00007f0acfcff000)
libMagickWand-7.Q16HDRI.so.7 => /opt/lib/libMagickWand-7.Q16HDRI.so.7 (0x00007f0acf9fa000)
libjpeg.so.9 => /opt/lib/libjpeg.so.9 (0x00007f0acf7c1000)
libz.so.1 => /lib64/libz.so.1 (0x00007f0acf5ab000)
libm.so.6 => /lib64/libm.so.6 (0x00007f0acf26b000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f0acf04d000)
libc.so.6 => /lib64/libc.so.6 (0x00007f0aceca2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0ad0239000)

依存関係の中で /optより始まるパスに置かれているのは今回ビルドしたライブラリです。libMagickCorelibMagickWandlibjpegは必要と思われます

ImageMagickコマンドに必要なファイルをコピーする 

実行ファイルをコピーします

host> cp -R volume/bin/magick \
      volume/bin/convert \
      layer/imagemagick/bin/

共有ライブラリをコピーします 

host> cp -R volume/lib/libMagickCore-7.Q16HDRI.so.7* \
      volume/lib/libMagickWand-7.Q16HDRI.so.7* \
      volume/lib/libjpeg.so.9* \
      layer/imagemagick/lib/

※ ファイルをそのままコピーするため -R オプションを指定しています

Lambda Layer の設定

serverless.ymlに以下のLambda Layerの設定を追加するだけです

layers:
  imageMagick:
    path: layer/imagemagick

参考: Serverless Framework - AWS Lambda Guide - Layers

npmをインストール

今回使用するnpmをyarnを使ってインストールします。※ aws-sdkはIntellijで補完させるためにインストールしていますが、Lambdaにプレインストールされているものを使えば必要ありません

yarn init
yarn add imagemagick
yarn add aws-sdk

Lambdaのパッケージ設定

serverless.ymlに不要なnpmファイルや作業用ファイルをパッケージされないようpackageの設定をします

package:
  exclude:
    - volume/**
    - nod_modules/**
    - '!node_modules/imagemagick/**'

Lambda の handler を作成

Lambdaのhadlerに設定する関数を作成します。以下は処理の概要です

  1. バケットにアップロードされたS3のキーをソースのキーとし、ソースのキーに outputというプレフィックスを付与し出力先のS3のキーを生成します
  2. ソースのキーをつかって S3 のオブジェクトをダウンロードします
  3. ダウンロードしたS3のオブジェクトをimagemagicknpmのresizeメソッドをつかって、画像のリサイズをおこないます
  4. リサイズされた画像のストリームを stdout(標準出力) より取得し、出力先のS3のキーにアップロードします

handler.js

'use strict';

const im = require("imagemagick")
const aws = require('aws-sdk')

const s3 = new aws.S3({
  apiVersion: '2006-03-01',
  signatureVersion: 'v4'
})

module.exports.resize_handler = async event => {
  const bucket = event.Records[0].s3.bucket.name
  const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '))
  const destKey = "output/" + srcKey

  const srcParams = {
    Bucket: bucket,
    Key: srcKey
  }

  const src = await s3.getObject(srcParams).promise()
  const contentType = src.ContentType
  const ext = contentType.split('/').pop()

  const res = await (() =>
      new Promise(resolve => {
        im.resize({
          srcData: src.Body,
          format: ext,
          width: 100,
          height: 100,
        }, (err, stdout, stderr) => {
          s3.putObject({
            Bucket: bucket,
            Key: destKey,
            Body: Buffer.from(stdout, 'binary'),
            ContentType: contentType
          }, (err, res) => {
            resolve(res)
          })
        })
      })
  )()
};

※ 説明の簡単のため、必要なエラー処理等を行っていません。実際にプロジェクトで使用する場合はエラー処理を忘れずに行ってください

Lambdaのhandlerの設定

serverless.ymlにhandlerの設定をしていきます

リサイズされたファイルをS3にアプロードするためのIAMの設定をします

provider:
  # 省略
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "S3:*"
      Resource: バケットのARN

つづいて以下2つの設定を行います

  • Lambda Layer を指定します。layersで指定した Lambda Layer は Layer名をキャメルケースにしサフィックスにLambdaLayerとつけた名前で参照できます。 今回の例だと imageMagick => ImageMagick => ImageMagickLambdaLayer と変形されます
  • S3にupload/で始まるキーでアップロードしたときに発生するイベントに作成したhandlerが起動されるよう設定します
functions:
  resize:
    handler: handler.resize_handler
    layers:
      - { Ref: ImageMagickaLambdaLayer }
    events:
      - s3:
          bucket: "バケット名"
          events:
            - "s3:ObjectCreated"
          rules:
            - prefix: "upload/"
          existing: true

デプロイ

slsを使ってAWS環境にデプロイします

sls deploy

S3に upload/プレフィックス以下にJPEGのファイルを置き、リサイズされた画像がoutput/プレフィックス以下にできれば完成です

さいごに

最初にご紹介した serverlesspub/imagemagick-aws-lambda-2: ImageMagick for AWS Lambda 2 runtimes ではmagick実行ファイルを静的リンクで作成し、共有ライブラリを個別に配置しなくてもよい方法で行っています。ですが、今回は勉強のため、あえて手順の多い共有ライブラリを使う方法で作成してみました

誰かのお役に立てれば幸いです