AWS Lambda で、動画からサムネ画像作成は、割とポピュラーなユースケースだと思いますが、Ruby runtime かつ Serverless Framework を組み合わせてのまとまった情報がなかったので、ここに記す。
この記事の主なトピック
- AWS Lambda (Ruby runtime)
- Serverless Framework
- AWS Lambda Layer
- FFmpeg
Serverless Framework とは?
- AWS Lambda などのデプロイやらLambda自体の設定等を楽に設定できるフレームワーク
- https://serverless.com/
- 以降は、単に
Serverless
と簡略して呼びます
基本的な開発からデプロイの流れは、
- Lambda 用のプロジェクトを作成
- YAML形式の設定ファイルを記述
- 関数そのものの実装
- 各(AWS, GCP, Azure, etc)サービスにデプロイ
Serverless によるLambda 関数のプロジェクト作成
まずは、serverless
をインストールします。
現時点(2019年05月10日時点)で、最新版の1.41.1
を使います。
ほんとは、グローバルにインストールする必要は必ずしもないのですが、簡略化のため。
$ npm i -g serverless
$ sls --version
1.41.1
Lambda関数のプロジェクトを作成
-
sls create
コマンドで、プロジェクトの雛形を作成します。 - 今回は、ランタイムがRuby(2.5)で、動画から、サムネ画像を作成するLambda関数を作成します。
-
--template
は、Serverless側が用意したテンプレートを使用します - https://serverless.com/framework/docs/providers/aws/guide/quick-start/
$ sls create --template aws-ruby --path lambda/capture_thumbnail_by_ruby
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/y.kojima/Desktop/lambda/capture_thumbnail_by_ruby"
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v1.41.1
-------'
Serverless: Successfully generated boilerplate for template: "aws-ruby"
$ ls
handler.rb serverless.yml
自動生成された各ファイルの中身はこんな感じ👇
require 'json'
def hello(event:, context:)
{ statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
service: capture_thumbnail_by_ruby # NOTE: update this with your service name
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"
provider:
name: aws
runtime: ruby2.5
# you can overwrite defaults here
# stage: dev
# region: us-east-1
# you can add statements to the Lambda function's IAM Role here
# iamRoleStatements:
# - Effect: "Allow"
# Action:
# - "s3:ListBucket"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
# - Effect: "Allow"
# Action:
# - "s3:PutObject"
# Resource:
# Fn::Join:
# - ""
# - - "arn:aws:s3:::"
# - "Ref" : "ServerlessDeploymentBucket"
# - "/*"
# you can define service wide environment variables here
# environment:
# variable1: value1
# you can add packaging information here
#package:
# include:
# - include-me.py
# - include-me-dir/**
# exclude:
# - exclude-me.py
# - exclude-me-dir/**
functions:
hello:
handler: handler.hello
# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details
# events:
# - http:
# path: users/create
# method: get
# - websocket: $connect
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
# - cloudwatchEvent:
# event:
# source:
# - "aws.ec2"
# detail-type:
# - "EC2 Instance State-change Notification"
# detail:
# state:
# - pending
# - cloudwatchLog: '/aws/lambda/hello'
# - cognitoUserPool:
# pool: MyUserPool
# trigger: PreSignUp
# Define function environment variables here
# environment:
# variable2: value2
# you can add CloudFormation resource templates here
#resources:
# Resources:
# NewResource:
# Type: AWS::S3::Bucket
# Properties:
# BucketName: my-new-bucket
# Outputs:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"
設定ファイルの編集
設定ファイル(serverless.yml
)で、まず設定したいことは、以下の2点。ほかは、後述します。
- LambdaからS3へ各操作を許可するためのIAMロールの設定
- どのS3バケットをトリガー対象として登録するか
まずは、不要なコメントを削除
service: capture_thumbnail_by_ruby
provider:
name: aws
runtime: ruby2.5
functions:
hello:
handler: handler.hello
IAMロールの設定と、Lambda自身のマシンリソースの設定を追記・変更します。
service: capture_thumbnail_by_ruby
provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
memorySize: 256
timeout: 180
# 開発、ステージング、本番環境で、デプロイを分ける際に、
# 'sls deploy --stage production'のように実行できる
# --stage を特に指定しない場合は、デフォルトで、開発環境に対してデプロイされる
stage: ${opt:stage, 'dev'}
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:GetBucketTagging"
- "s3:GetObjectTagging"
- "s3:PutObjectTagging"
- "s3:PutBucketNotification"
Resource:
- "arn:aws:s3:::*"
functions:
captureThumbnail:
handler: handler.capture_thumbnail
description: Capture thumbnail image from video file
events:
- s3:
bucket: test
event: s3:ObjectCreated:*
rules:
- prefix: cache/test/
- suffix: .mp4
サムネ画像生成の関数の実装
- 外部ライブラリの
aws-sdk-s3
が必要なので、Gemfile
とGemfile.lock
が、まず必要になります。
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "aws-sdk-s3", "~> 1"
Gemfile
を作成したら、bundle install
を実行
サムネ画像を作成するための関数の実装
require 'json'
require 'aws-sdk-s3'
require 'logger'
require 'open3'
def capture_thumbnail(event:, context:)
s3 = Aws::S3::Client.new
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
logger.info("Start to capture thumbnail image for s3://#{bucket}/#{key}")
basename = File.basename(key)
signer = Aws::S3::Presigner.new
url = signer.presigned_url(:get_object, bucket: bucket, key: key)
# 後述する AWS Lambda Layer を使用して、FFmpegを Lambda 環境にインストールしているので、
# 'opt/'以下に格納されたffmpegの実行ファイルを使用する
capture_cmd = "/opt/ffmpeg/ffmpeg -i \"#{url}\" -ss 3 -vframes 1 -f image2 /tmp/#{basename}.jpg"
log_output, error, status = Open3.capture3(capture_cmd)
logger.info("Capture thumbnail is completed")
logger.info(log_output)
logger.info(status)
unless status.success?
logger.error(error)
raise
end
s3.put_object({
acl: 'public-read',
body: File.open("/tmp/#{basename}.jpg"),
bucket: bucket,
key: "#{key.sub('movie', 'thumbnail')}.jpg",
content_type: 'image/jpeg'
})
logger.info("Upload is completed!")
rescue => e
logger.error("#{e.class}:#{e.message}")
raise e
ensure
clear_cache
end
private
def clear_cache
logger.info(Open3.capture3('whoami')[0])
logger.info(Open3.capture3('rm -vfr /tmp/*')[0])
logger.info(Open3.capture3('ls -l /tmp/')[0])
end
def logger
@logger ||= Logger.new(STDOUT)
end
FFmpegの実行自体は、外部コマンドで実行するため、実行前に、Lambda内に、FFmpegの実行ファイルをデプロイしておく必要があります。
単純な方法としては、一度手元のローカル環境で、FFmpegをインストールして、それをLambdaのデプロイに含める方法があります。
しかし、この方法は、FFmpeg本体をインストールする必要があるので、Lambdaのファイルサイズ増加につながるため、デプロイ時間に悪影響を与える。
また、他の Lambda関数でも、FFmpegを利用したい場合に、各関数毎に、FFmpegをインストールする必要があるので、DRYではない。
そこで、AWS Lambda Layer が問題を解決する。
AWS Lambda Layer とは?
一言で言えば、複数のLambda関数でライブラリを共有できる仕組みです。
Lambda Layerの基本的な仕組みを確認する より引用
- https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html
- https://dev.classmethod.jp/cloud/aws/lambda-layer-basics-how-it-works/
FFmpegを含んだ AWS Lambda Layer を Lambda関数とは、別でデプロイしておくことで、必要な時に Lambda関数が、Layerを利用するといったことが可能となる。
もちろん、Serverless
は、AWS Lambda Layer をサポートしているので、serverless.yml
にLayer の設定を追加する。
service: capture_thumbnail_by_ruby
provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
memorySize: 256
timeout: 180
# 開発、ステージング、本番環境で、デプロイを分ける際に、
# 'sls deploy --stage production'のように実行できる
# --stage を特に指定しない場合は、デフォルトで、開発環境に対してデプロイされる
stage: ${opt:stage, 'dev'}
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:GetBucketTagging"
- "s3:GetObjectTagging"
- "s3:PutObjectTagging"
- "s3:PutBucketNotification"
Resource:
- "arn:aws:s3:::*"
functions:
captureThumbnail:
handler: handler.capture_thumbnail
description: Capture thumbnail image from video file
events:
- s3:
bucket: test
event: s3:ObjectCreated:*
rules:
- prefix: cache/test/
- suffix: .mp4
layers:
ffmpeg:
path: layer
layer
というディレクトリを作成しておきます。
mkdir layer
このままだと、AWS Lambda Layer のスペースができただけなので、Layerをデプロイする前のフック処理として、Layer内に FFmpeg をインストールする前処理が、必要になります。
デプロイ前のフック処理を行うために、Serverless
の機能であるPlugins
を使用します。
Plugins について
-
Serverless
のコマンドとして使える、自前のカスタムコマンドを作成するための機能です。 - 今回で言えば、「デプロイ前に FFmpeg をインストールして、
layer
以下に配置する」 というカスタムコマンドを作る必要があります。 - Pluginsの実装自体は、JavaScript でのみ作ることができます。
- https://serverless.com/framework/docs/providers/aws/guide/plugins/
Plugins用のディレクトリとファイルを作成します。
mkdir plugins
touch InstallFFmpeg.js
Pluginsの実装内容はこちら👇
重要なのは、this.hooks
の部分で、ここでデプロイ前に FFmpeg をダウンロードするフック処理を設定します。
'use strict';
const execSync = require('child_process').execSync
class InstallFFmpeg {
constructor() {
this.commands = {
deploy: {
lifecycleEvents: [
'resources'
]
},
};
this.hooks = {
'before:deploy:resources': this.beforeDeployResources
};
}
beforeDeployResources() {
console.log('execute hook!')
const ffmpegURL = 'https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz'
// 既に Layer が存在している場合は、一旦削除する
execSync('rm -fr layer/*', (error, stdout, stderr) => {
if (error) { console.log(stderr); }
console.log(stdout)
})
// FFmpegをインストール
execSync(`curl ${ffmpegURL} | tar zxv -C layer && cd layer && mv ffmpeg-*-*-static ffmpeg`, (error, stdout, stderr) => {
if (error) { console.log(stderr); }
console.log(stdout)
})
console.log('finished hook!')
};
}
module.exports = InstallFFmpeg;
Plugin ができたので、作成した Plugin を使用するようserverless.yml
に変更を加えます。
service: capture_thumbnail_by_ruby
provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
memorySize: 256
timeout: 180
# 開発、ステージング、本番環境で、デプロイを分ける際に、
# 'sls deploy --stage production'のように実行できる
# --stage を特に指定しない場合は、デフォルトで、開発環境に対してデプロイされる
stage: ${opt:stage, 'dev'}
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:GetBucketTagging"
- "s3:GetObjectTagging"
- "s3:PutObjectTagging"
- "s3:PutBucketNotification"
Resource:
- "arn:aws:s3:::*"
functions:
captureThumbnail:
handler: handler.capture_thumbnail
description: Capture thumbnail image from video file
events:
- s3:
bucket: test
event: s3:ObjectCreated:*
rules:
- prefix: cache/test/
- suffix: .mp4
layers:
ffmpeg:
path: layer
plugins:
localPath: './plugins'
modules:
- InstallFFmpeg
これで、デプロイまでの準備が完成🚀
デプロイ
デプロイ作業としては、以下の作業が必要になります。
- Lambda 本体のデプロイ
- Lambda の実行トリガーの登録
Lambda 本体のデプロイ
sls deploy
// ステージング用のLambdaをデプロイする場合
sls deploy --stage staging
// 本番用のLambdaをデプロイする場合
sls deploy --stage production
※ 💣既存のS3バケットをトリガー対象にする場合
- そのままでは、デプロイコマンドが失敗します
- 理由としては、
Serverless
は、デフォルトでは、トリガー対象のS3バケットを指定する場合、新規のS3バケットを作成しようとするためです。
- 理由としては、
$ sls deploy
Serverless Error ---------------------------------------
Serverless plugin "serverless-plugin-existing-s3" not found. Make sure it's installed and listed in the "plugins" section of your serverless config file.
Get Support --------------------------------------------
Docs: docs.serverless.com
Bugs: github.com/serverless/serverless/issues
Issues: forum.serverless.com
Your Environment Information ---------------------------
OS: darwin
Node Version: 8.15.0
Serverless Version: 1.41.1
新規のS3バケットではなく、既存のS3バケットをトリガー対象にしたい場合は、serverless-plugin-existing-s3
というPluginsが必要になります。
このライブラリは、npmパッケージなので、package.json
を作成します。
yarn init or npm init
そして、serverless-plugin-existing-s3
をインストール
yarn add serverless-plugin-existing-s3
再度、sls deploy
します
$ sls deploy
execute hook!
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0x ffmpeg-4.1.1-amd64-static/
x ffmpeg-4.1.1-amd64-static/GPLv3.txt
x ffmpeg-4.1.1-amd64-static/manpages/
......................
finished hook!
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service captureThumbnailRubyForTest.zip file to S3 (13.49 KB)...
Serverless: Uploading service ffmpeg.zip file to S3 (51.32 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..................
Serverless: Stack update finished...
Service Information
service: captureThumbnailRubyForTest
stage: dev
region: ap-northeast-1
stack: captureThumbnailRubyForTest-dev
resources: 6
api keys:
None
endpoints:
None
functions:
captureThumbnail: captureThumbnailRubyForTest-dev-captureThumbnail
layers:
ffmpeg: arn:aws:lambda:ap-northeast-1:0123456:layer:ffmpeg:1
デプロイが完了すると、AWSコンソールの方にも、関数が追加される。
トリガー対象のS3バケットはまだ登録されていないので、sls s3deploy
で、トリガー対象を登録します。
先程、serverless-plugin-existing-s3
をインストールしたので、既存のS3バケットをserverless.yml
にトリガー対象として登録するために、設定内容を少し変更させます。
service: capture_thumbnail_by_ruby
provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
memorySize: 256
timeout: 180
stage: ${opt:stage, 'dev'}
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:GetBucketTagging"
- "s3:GetObjectTagging"
- "s3:PutObjectTagging"
- "s3:PutBucketNotification"
Resource:
- "arn:aws:s3:::*"
functions:
captureThumbnail:
handler: handler.capture_thumbnail
description: Capture thumbnail image from video file
events:
+ - existingS3:
+ bucket: ${file(./config.${self:provider.stage}.yml):trigger.bucket}
+ events:
+ - s3:ObjectCreated:*
+ rules:
+ - prefix: cache/test/
+ - suffix: .mp4
layers:
ffmpeg:
path: layer
plugins:
localPath: './plugins'
modules:
- InstallFFmpeg
+ - serverless-plugin-existing-s3
+ package:
+ exclude:
+ - yarn-error.log
+ - yarn.lock
+ - package.json
本番、ステージング、開発環境毎に、S3バケットを分けている場合は、bucket
の部分が動的に指定できるように、各環境毎に、バケット名を設定したYAMLファイルを Lambdaプロジェクトのルートディレクトリに、用意します。
- config.dev.yml
- config.staging.yml
- config.production.yml
設定内容は、各環境毎のバケット名
trigger:
# bucket: dev or staging or production
bucket: production
再度、sls s3deploy
を実行
$ sls s3deploy
Serverless: beforeFunctions --> building ...
Serverless: beforeFunctions <-- Complete, built 1 events.
Serverless: functions --> prepare to be executed by s3 buckets ...
policyId exS3-v2-captureThumbnailRubyForTest-dev-captureThumbnail-crevo-test
Serverless: functions <-- built 0 events across 1 buckets.
Serverless: beforeS3 -->
Serverless: beforeS3 <--
Serverless: s3 --> initiate requests ...
Serverless: s3 <-- Complete 1 updates.
成功した場合、指定したS3バケットがトリガー対象として登録されます。
これで、トリガー対象のS3バケットに、mp4などの動画をアップロードすると、Lambdaが実行して、サムネ画像が作成されます。
(CloudWatch で確認してみるでもよいでしょう!)
※ 💣もし、以下のようなエラーが出た場合
$ sls s3deploy
Serverless: beforeFunctions --> building ...
Serverless: beforeFunctions <-- Complete, built 1 events.
Serverless: functions --> prepare to be executed by s3 buckets ...
policyId exS3-v2-captureThumbnailRubyForTest-dev-captureThumbnail-test-bucket
Serverless: functions <-- built 0 events across 1 buckets.
Serverless: beforeS3 -->
Serverless: beforeS3 <--
Serverless: s3 --> initiate requests ...
Error --------------------------------------------------
test-bucket Configuration is ambiguously defined. Cannot have overlapping suffixes in two rules if the prefixes are overlapping for the same event type.
For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.
Get Support --------------------------------------------
Docs: docs.serverless.com
Bugs: github.com/serverless/serverless/issues
Issues: forum.serverless.com
Your Environment Information ---------------------------
OS: darwin
Node Version: 8.15.0
Serverless Version: 1.41.1
他の Lambda 関数で、同じS3バケットがトリガーとして登録されているので、登録を無効にする必要があります。
まとめ
- Serverless Framework を使うと、AWSのデプロイ周りが楽
- YAML形式なので、公式ドキュメントとにらめっこしながら、なんとなく設定できるので、学習コストはそこまで高くない印象
- 各機能の説明はわかりやすいが、複数の機能を組み合わせた例などが、ドキュメントだけだと不足気味な印象。。。
AWS Lambda を Rubyで書けるの、やっぱ楽しいな✨