🥐

LambdaでImageMagickを使うためのLambda Layer構築手順

2023/09/12に公開

動機

AWS Lambdaは特定のルールに沿ったコードを登録しておきさえすれば、必要になったタイミングでAWS側で用意されたコンテナ上でプログラムが実行される便利なサービスです。

ただ、それゆえにC言語等で実装されたネイティブなライブラリに依存するコードを書いても、そのライブラリが Lambdaコンテナ上に存在しないために意図したように動いてくれません。

今回、S3に画像が保存された時点でトリガーされて小さなサムネイル画像を生成し別のS3バケットに保存するLambda関数を作るにあたって、ImageMagickに依存する必要が出てきたことで、このケースでどのようにするべきか調査したので書き残します。

なお、もしあなたが node10 を使うつもりであればこっちをデプロイして使うのが早道です。
https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~image-magick-lambda-layer

解決策

解決策の一つとして Lambda Layer を使う、というものがあります。
Lambda Layerは、Lambda関数内で使用するファイル(ライブラリや実行ファイル等)をあらかじめレイヤーとして登録しておいて、Lambda関数でこのレイヤーを指定すると、そこに含まれるライブラリや実行ファイルに関数からアクセスできるというものです。

考え方

Lambda Layerは、単純に1つのディレクトリにファイルやディレクトリを保存したものを zip で固めて登録します。

ディレクトリ以下にあるファイルには /opt 以下にあるものとしてアクセスが可能です。

例:

以下の内容のディレクトリを zip で圧縮して登録すると...

test.txt
inu/neko.png

Lambda関数からは

/opt/test.txt
/opt/inu/neko.png

としてアクセスできます。

今回の考え方としては、ImageMagickや付随するライブラリを全て一つのディレクトリにまとめて入れて、それをzipで固めてLambda Layerとして登録することで、Lambda関数からは /opt/lib/opt/bin からライブラリや実行ファイルにアクセスできる環境をつくります。

手順

Amazon Linux 2 を立ち上げ

ImageMagickをビルドするための EC2インスタンスを立ち上げる。Dockerを使っても良いし、EC2を使ってもいいけど、今回はEC2を使う。

このインスタンスはLambdaと同じ環境でライブラリを構築するためのもので、本番環境では使用しない。したがって今回の作業が終わったら削除していい。

立ち上げ時に選択する環境を自分が使う予定のLambdaの環境に合わせる。

OSのバージョンは公式の「Lambda ランタイム」をみて、自分の使用予定のランタイムが使っている Amazon Linux のバージョンを確認。

今回は Ruby3.2 を使う予定なので Amazon Linux2 であることがわかる。

また、Lambdaのアーキテクチャは arm64 を選択する予定なので、こちらも同様に arm64 のEC2を立ち上げる。

sshでログイン

ssh -i [sshキーへのパス)] ec2-user@[EC2インスタンスのIPアドレス]

依存ライブラリ等のインストール

Lambda Layerに加えたファイル類は、Lambda上からは /opt 以下に配置される。
したがって、ImageMagick を /opt 以下にインストールして、それを zip で固めて LambdaLayerに含める。

先ほど作ったEC2インスタンス上で以下を実行.

開発者パッケージをインストール

ビルドに必要なパッケージをインストール

sudo yum update -y
sudo yum groupinstall "Development Tools" 

libjpegをインストール

公式サイトからダウンロード

tar xvzf jpegsrc.v9e.tar.gz
cd jpeg-9e/
./configure --prefix=/opt CPPFLAGS=-I/opt/include LDFLAGS=-L/opt/lib --disable-dependency-tracking --enable-shared
make
sudo make intall

libpngをインストール

SourceForgeからダウンロード

tar xvzf libpng-1.6.40.tar.gz
cd libpng-1.6.40/
./configure --prefix=/opt CPPFLAGS=-I/opt/include LDFLAGS=-L/opt/lib --disable-dependency-tracking --enable-shared
make 
sudo make install

zlibをインストール

ImageMagickはPNG画像フォーマットを扱う時に内部でzlibを使うので、これもインストール。

公式サイトからダウンロード

./configure --prefix=/opt
make 
sudo make install

ImageMagickをビルドしてインストール

公式サイトからダウンロード。

wget https://imagemagick.org/archive/ImageMagick.tar.gz

ビルド

export PKG_CONFIG_PATH=/opt/lib/pkgconfig
./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 \
	    --without-dmr \
	    --without-heic \
	    --without-jbig \
	    --without-lcms \
	    --without-openjp2 \
	    --without-lqr \
	    --without-lzma \
	    --without-pango \
	    --without-raw \
	    --with-rsvg \
	    --without-tiff \
	    --disable-openmp \
	    --disable-dependency-tracking

make

/opt/ImageMagick 以下にインストール。

sudo make install

動作テスト

サーバー上に test.png というPNGフォーマットの画像を用意して以下を実行

/opt/bin/convert test.png  -resize x500 test-resized.png

エラーが表示されず、 test-resized.png というファイルが生成されていれば成功。

Lambda Layer用にパッケージングする。

awsrh という2つのディレクトリは、はじめから /opt 以下にあって今回は必要ないので、除外してzipで固める。

zip -r ~/imagemagick-layer.zip . -x "aws/*" "rh/*"

使い方

出来上がった imagemagick-layer.zip を AWSコンソールの レイヤー 画面からアップロードするなり、SAMテンプレートで指定するなりして Lambda Layerを構築する。

ちなみに SAMテンプレートファイルの 'lambda-layer/imagemagick-layer.zip' に配置した場合は、Lambda Layerを構築するSAMテンプレートはこんな感じになる。

  ImageMagickLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: ImageMagickLayer
      Description: ImageMagick layer for lambda (ARM64)
      ContentUri: ./lambda-layer/imagemagick-layer.zip
      CompatibleRuntimes:
        - ruby3.2

ランタイムは自分の使いたい環境に合わせて python なり nodejsなりを指定する。
これで出来上がったLambda Layerを、使いたいLambda関数に紐づける。
コンソールから行う場合は、目的のLambda関数を開いて、 コード タブの一番下の レイヤー から追加できる。

Lambda関数の実装の仕方

今回のImageMagickに関しては、 Lamba関数から見て /opt/bin/convert という実行ファイルにアクセスできる。

プログラミング言語別のライブラリからImageMagickを使うのもいいが、最も簡単なのは外部コマンドの呼び出しで実行してしまう方法。

rubyの場合

input_image_path = "/tmp/neko.png"
output_image_path = "/tmp/neko-thumbnail.png"
result = `convert #{input_image_path} -resize x512 #{output_image_path}`

のようにすれば、高さ512ピクセルの画像が生成される。
あとはこれをS3バケットに保存するなりすればOK。

実際にS3に保存された画像ファイルを、縦1024ピクセルになるように修正して別のS3バケットに保存するLambda関数のサンプルは以下のようになる.


require 'aws-sdk-s3'
THUMBNAIL_SIZE_HEIGHT = 1024

# ImageMagickを使ったリサイズ処理(高さが1024ピクセルになるようにし、PNG,JPEGフォーマットに対応)
def handler(event:, context:)
  s3_client = Aws::S3::Client.new()

  # イベントソースとして指定したS3から保存された画像を取り込む
  key = event['Records'][0]['s3']['object']['key']
  puts "Original Image: #{key}"

  # 一時ファイルに画像を保存
  original_bucket = "original-image.sample-bacuket.co.jp"
  
  # key は "/"区切りのパスになっているので、一時ファイル名として使うために "/"を"-"に置換する。
  tmp_key = key.gsub("/", "-")  
  original_image_path = "/tmp/#{tmp_key}"
  
  s3_client.get_object(
    :bucket => original_bucket,
    :key => key,
    :response_target => original_image_path
  )

  # 画像を高さが1024ピクセルになるようにリサイズして /tmp 以下に書き出す。
  # 外部コマンドとして convert を実行。 /opt/bin にはパスが通ってるので相対パスでOK.
  thumbnail_image_path = "/tmp/#{tmp_key}-thumbnail"
  system "convert #{original_image_path} -resize x#{THUMBNAIL_SIZE_HEIGHT} #{thumbnail_image_path}"

  # リサイズした画像のバイナリデータをサムネイル用S3へアップロードする。
  s3_resource = Aws::S3::Resource.new()
  thumbnail_bucket = "thumbnail-image.sample-bacuket.co.jp"
  object = s3_resource.bucket(thumbnail_bucket).object(key).upload_file(thumbnail_image_path)

  # 一時ファイルを削除
  File.delete(original_image_path)
  File.delete(thumbnail_image_path)
end

Discussion