Serverless Framework & DockerによるローカルフレンドリーなLambda開発・運用
会社のチームメンバーにLambdaの開発・デプロイ方法について共有するために書いた記事ですが、Lambdaを触ったことない人にも有益そうな内容なので、Zennに投稿してみました🙏
今回作りたいLambda
- 指定したS3バケットに画像がアップロードされたら、リサイズして同じまたは別のS3バケットにアップロードする
完成リポジトリ
Runtime
- Ruby2.7
前準備
AWSアカウントが作成済み、AWS CLIインストール済み
- アクセスキーIDとシークレットアクセスキーを事前に設定する必要あり
$ aws configure
AWS Access Key ID [****************]:
Dockerのインストール
- 筆者がインストールしているバージョンがたまたまこれなので、必ずしも一致している必要はありません。
$ docker --version
Docker version 20.10.5, build 55c4c88
Serverless Framworkについて
Lambdaをデプロイするまでに必要な各設定を簡単にできるツールです。
他にも、AWS公式のSAMやTerraformなど他のlaCでも代用は可能です。
おそらくLambdaにフォーカスするなら、Serverless FrameworkかSAMの2択になるかなと思います。
これら2つの詳細な比較は今回割愛しますが、以下の条件いずれかに該当するならServerless Frameworkを使ったほうが良いかと考えます。
- 既存のS3バケットをLambda実行のイベントトリガーとして登録したい
- AWS Lambdaの他にCloud Functions(GCP)なども使っておりマルチクラウドな環境
- 手慣れている
一番上の条件が自分は出くわすことが多いので、Serverless Frameworkを使いがちです。
どうやらSAMはデプロイコマンド実行時にツール内部で、CloudFormationを使ってリソースを新たに構築する関係上、イベントトリガーで指定したS3バケットを新規作成しようと試みるが、S3のバケット名はグローバルにユニークでないといけないため、既存のS3バケットと名前が重複し失敗します。
そのため、新規にAWS環境を構築するような初期フェーズなら良いのでしょうが、すでに既存のAWSリソースが構築済みのような状況でLambdaを追加したいという場合だと、Serverless Frameworkの1択になるかなと思います。
※補足: SAMとServerless Frameworkの既存S3バケットのイベント通知について
SAMのS3イベントトリガー関連のロードマップissueを見ていると、2021年5月時点ですとまだ既存のS3をトリガーとして登録することはできなさそうです。
一方のServerless Frameworkでは既存のS3をイベントトリガーにどうやって登録しているかと言うと、Serverless Framework自身がデプロイコマンド実行時に、既存S3バケットのイベントをpub/subするEventBridgeなどを作成するためにCloudFormationのカスタムリソースで実現しています。
Serverless Framwrokを実行できるdocker-compose環境を構築する
- ローカルでServerless Framworkをインストールしてもいいのですが、チーム開発を想定してdocker-compose化しておくことを推奨します。
まずは、以下のようなディレクトリとファイルを作成
$ mkdir resize-lambda-serverless
$ cd resize-lambda-serverless
$ tree -a -I .git
.
├── .gitignore
├── Dockerfile.dev
├── docker-compose.yml
├── docker.env
└── docker.env.sample
Dockerfile.dev
使用するベースイメージはLambdaの環境に限りなく近いdocker-lambdaのイメージを使用します。
FROM lambci/lambda:build-ruby2.7
# serverless frameworkのインストール
RUN curl -sL https://rpm.nodesource.com/setup_12.x | bash - \
&& yum -y install nodejs && yum -y clean all \
&& npm install -g serverless
WORKDIR /var/task/
CMD ["/bin/bash"]
docker-compose.yml
version: '3'
services:
serverless:
build:
context: ./
dockerfile: Dockerfile.dev
volumes:
- .:/var/task
env_file:
- docker.env
docker.env
- docker-compose内でAWSの各クレデンシャル情報が使えるように設定しておきます。
- 💣Gitでremote pushする際はこのファイルは必ず gitignore対象にしておきましょう!
AWS_ACCESS_KEY_ID=xxx
AWS_DEFAULT_REGION=ap-northeast-1
AWS_SECRET_ACCESS_KEY=xxx
.gitignore
docker.env
### Serverless ###
# Ignore build directory
.serverless
Serverless FrameworkのDocker環境を起動
$ docker-compose run --rm serverless sh
sh-4.2# ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
sh-4.2# sls -version
Framework Core: 2.39.2
Plugin: 4.5.3
SDK: 4.2.2
Components: 3.9.2
Ruby Runtime Lambdaのテンプレート作成
sh-4.2# serverless create --template aws-ruby --name resize-lambda-serverless
sh-4.2# yum -y install tree
sh-4.2# tree -a -I .git
.
├── docker-compose.yml
├── docker.env
├── docker.env.sample
├── Dockerfile.dev
├── .gitignore
├── handler.rb
├── .npmignore
└── serverless.yml
動作確認用に serverless.yml
を以下のように変更。
service: resize-lambda-serverless
frameworkVersion: '2'
provider:
name: aws
runtime: ruby2.7
region: ap-northeast-1
lambdaHashingVersion: 20201221
functions:
hello:
handler: handler.hello
events:
- httpApi:
path: /hello
method: get
package:
exclude:
- .git
- .gitignore
- docker.env
- docker.env.sample
- docker-compose.yml
- Dockerfile.dev
これで最低限の準備は整ったので、試しにデプロイ
sh-4.2# sls deploy
...省略
Serverless: Stack update finished...
Service Information
service: resize-lambda-serverless
stage: dev
region: ap-northeast-1
stack: resize-lambda-serverless-dev
resources: 11
api keys:
None
endpoints:
GET - https://xxx.execute-api.ap-northeast-1.amazonaws.com/hello
functions:
hello: resize-lambda-serverless-dev-hello
layers:
None
Native extensionsに依存するgemを使用したRubyの開発環境のセットアップ
今回のユースケースであるリサイズ処理を実装したい場合、何からのGemが必要になるのでbundlerまわりを整えていきます。
sh-4.2# bundler init
Writing new Gemfile to /var/task/Gemfile
Gemfileにリサイズ処理に必要なgemを追加します。
今回は、ImageMagickではなく libvipsを使うので image_processing
gemを追加
Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "aws-sdk-s3"
gem "image_processing"
group :development do
gem "rake"
end
group :test do
gem "pry"
gem "minitest"
end
Gemfile変更後に、Lambda内にgemを含めるための設定を行います。
sh-4.2# bundle config set path "vendor/bundle"
sh-4.2# bundle install
これで準備万端と思いきや、image_processing
gemを使ったリサイズ処理を実行することはできません。
例えばhandler.rb
内で、require image_processing/vips
をして、bundle exec ruby handler.rb
を実行すると、エラーになります。
sh-4.2# bundle exec ruby handler.rb
...
Could not open library 'libvips.so.42': libvips.so.42: cannot open shared object file: No such file or directory
これはimage_processing/vips
がlibvipsに依存しているためで現在のDocker環境にlibvipsをインストールする必要があります。
Lambdaでのnative extensionsの取り扱いについて
今回のケースのようにC言語などに依存するライブラリを使用する場合、Lambdaでは以下のどちらかの対応が必要になります。
- Dockerを使う
- Lambda Layerを使う
Docker を使う
2020年12月からLambdaは、Dockerをサポートしたのでnative extensionsなライブラリを含んだDockerイメージを作ってしまうという方法があります。
例えば今回のケースであれば、以下のような流れで対応できそうです。
- libvipsをインストールしたLambda実行用のDockerイメージを作成
- ECRに作成したDockerイメージをpush
- ECRからイメージをpullしてLambdaを実行
使用するDockerイメージはAWS公式がAmazonLinux2ベースのLambda用イメージを提供しているのでそれを使うのが良さそうです。
ただし、libvips及びその他諸々の周辺ミドルウェア(libpngなど)も自分で Dockerfile にインストールの定義を追加していくかつLambdaで実行可能な設定を追加する必要があります。
個人的には、自分で必要なミドルウェアを毎度定義していくのはしんどいので後述するLambda Layerを使うほうが良いと思います。
Lambda Layer を使う
詳細な説明は、公式ドキュメントやクラスメソッドさんの記事に譲りますが、ざっくりいうとメインロジックの実装とは別に共通して使用する依存ライブラリなどをlayerという形で別にデプロイして、他のLambdaからlayerにあるライブラリなどを使用することができるというものです。
Lambda Layerが素晴らしいのは、Lambda自体のサイズ肥大化を抑えられるだけでなく既にAWS公式やいろんな人がDockerイメージやOSSリポジトリなどで様々な依存ライブラリをまとめたLayerを公開しているなどエコシステムが整っていることです。
目当てのlibvipsもありますし、その他Layerもまとめられています。
customink/ruby-vips-lambda を使えば、libvipsを含んだLambda Layerのデプロイもすぐにできます。
$ git clone git@github.com:customink/ruby-vips-lambda.git
$ cd customink/ruby-vips-lambda
東京リージョンにデプロイするため、bin/deploy
を少しいじります。
#!/bin/bash
set -e
./bin/build
export VIPS_VERSION=$(cat share/VIPS_VERSION)
export LAYER_NAME="rubyvips${VIPS_VERSION//./}-27"
- export AWS_REGION=${AWS_REGION:=us-east-1}
+ export AWS_REGION=${AWS_REGION:=ap-northeast-1}
aws lambda publish-layer-version \
--region $AWS_REGION \
--layer-name $LAYER_NAME \
--description "Libvips for Ruby FFI." \
--zip-file "fileb://share/libvips.zip"
そして、Layerをデプロイ
$ bin/deploy
Layerを使ったリサイズ処理のLambda
handler.rb
require_relative "./lib/image_service"
def handler(event:, context:)
key = event["Records"][0].dig("s3", "object", "key")
resizer = ImageService::Resizer.new(key, 280)
resizer.resize!
end
リサイズ処理自体は、POROで別に分けることでテストがしやすくなります。
serverless.yml
- S3への操作権限付与
- 使用するlayerの指定
- イベントトリガーに登録するS3バケットの指定
service: resize-lambda-serverless
frameworkVersion: '2'
provider:
name: aws
runtime: ruby2.7
region: ap-northeast-1
lambdaHashingVersion: 20201221
+ iamRoleStatements:
+ - Effect: "Allow"
+ Action:
+ - "s3:*"
+ Resource:
+ - "arn:aws:s3:::${指定したバケット名}/*"
functions:
image_resizer:
handler: handler.handler
+ layers:
+ - arn:aws:lambda:ap-northeast-1:アカウントID:layer:デプロイしたlayerのarn
events:
+ - s3:
+ bucket: ${指定したバケット名}
+ events:
+ - s3:ObjectCreated:*
+ rules:
+ - prefix: ${指定したディレクトリ名}
+ existing: true
package:
exclude:
- .git
- .gitignore
- docker.env
- docker.env.sample
- docker-compose.yml
- Dockerfile.dev
これでsls deploy
を実行した後に指定したS3バケットに画像をアップロードすれば、リサイズされた画像が追加されます。
ローカルフレンドリーなLambda開発
customink/ruby-vips-lambda を使って native extensionsなライブラリをLayerで外だしできましたが、ローカル上で依存ライブラリを含んだ状態でLambda上の処理をデバッグがしたい場合は、LayerそのものをDockerイメージ化しておくのがおすすめです。
幸い、customink/ruby-vips-lambda
はDockerfileがあるので、それを手元でプロジェクトのDockerfileにコピペしてもよいかもしれません。
今回であれば、Dockerfile.dev
にcustomink/ruby-vips-lambda
のDockerfile にあるlibvipsのインストールなどの定義を追加します。
そして、docker-compose環境内で以下のようにデバッグするなどが一番手軽かもしれません。
# 手元でデバッグするために一時的にコメントアウト
# def handler(event:, context:)
def handler
binding.irb
end
sh-4.2# bundle exec ruby handler.rb
docker-lambdaのREADMEにあるように、Dockerコンテナーを起動してcurlでLambdaのエンドポイントを叩いて動作確認もした方が良いかもしれませんが、docker-composeでnative extentions含めた環境が再現できればひとまず十分ではないかと個人的には考えます。
Dockerを活用したLambdaの開発には、docker-lambdaの他に yumda
もあり、こちらはAmazonLinux2ベースのイメージにyum経由で簡単に依存ライブラリを追加できるのでLambda LayerのDocker化が簡単にできそうです。
まとめると以下のアプローチが考えられるかなと思います。
-
lambci/build-xxx
のDockerイメージを使ってLambda, Layer環境のdocker-compose化 -
docker-lambda
のコンテナーを起動させて動作確認 - Lambda本体をdocker-lambdaのイメージを使い、layerはyumdaで別にイメージ作成する
Discussion