AWSのキャッチアップをしながら、S3にアップロードした動画のサムネイルを抽出するLambdaを作った
はじめに
こんにちは。株式会社バニッシュ・スタンダードでサーバーサイドエンジニアをやっているhidechaeです。
弊社にはインフラチームがあるので、アプリケーションの開発チームがゴリゴリインフラを触る機会は少ないのですが、一部Lambdaなど実装が絡むところは、サーバーサイドエンジニアがSAMを書いたりしています。
僕個人としては、5年前くらいまではAWSを業務で触っていたりしましたが、ここ数年はインフラチームが作ってくれたものを見ているだけで細かいことは全然キャッチアップできてない状態でした。
そんな中、久々にLambda作っていて色々と詰まったので記録に残しておきます。誰かの役に立つといいな…
やりたいこと
今回やりたかったことは動画のサムネイルを作成です。
実装方針
まず、大きな方針として以下の3案あります。
- 動画アップロード時にサーバーサイドでffmpegを使ってサムネイルを作成する
- S3に動画をアップロードした後、非同期でLambdaでffmpegを使ってサムネイルを作成する
- Elemental MediaConvertを使ってサムネイルを作成する
前提としてサーバーサイドはgoで書かれており、Fargateで動いています。
1に関しては、構成が最もシンプルですが、Dockerイメージのサイズが大幅に大きくなってしまうため、コスト面や起動速度の低下などデメリットが大きいということで却下しました。
3に関しては、Elemental MediaConvertではサムネイルのみの作成ができず、動画の変換処理とセットになってしまうため、今回の要件には合わないということで却下しました。
結果的に2のLambda上でffmpegを動かすパターンで実装することにしました。
アーキテクチャ
サムネイル作成までの流れは以下のようになります。
- ClientからS3に動画をアップロードする
- S3のイベント通知をEventBridgeに送信する
- EventBridgeがLambdaを起動する
- Lambdaがサムネイルを作成する
- 作成したサムネイルをS3にアップロードする
途中で詰まって試行錯誤したこと
初歩的な躓きが多いのですが一通り記録しておこうと思います。
SAMでは既存のS3バケットにS3イベント通知設定出来ない
当初、S3イベントからLambdaを起動しようと思って作業していました。
作業している中で、SAMで構築する場合、template.yaml内で既存のS3バケットにS3イベントを設定することは出来ず、新規S3バケットを構築する必要があることが分かりました。
そのため、EventBridge経由でLambdaを起動することにしました。
CloudTrailは不要らしい
調べていると、S3 → CloudTrail → EventBridge といった構成のサンプルがよく出てくるなぁと思っていましたが、今はCloudTrail証跡なしでEventBridgeで直接S3イベントを拾えるようになっているようです。
S3のイベントがEventBridgeに飛ばない
S3のバケットのプロパティでEventBridgeへのイベント通知設定をオンにしなければ通知は飛びません。
僕は気づかず結構な時間を無駄にしました…。
権限が色々足りなくて動かない
まず、EventBridgeがLambdaを起動する権限が必要でした。
Action:
- lambda:InvokeFunction
また、Lambdaを起動するときには、CloudWatch Logsの権限が必要です。
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
あとは、LambdaがS3にアクセスするのでS3へのアクセス権限が必要となります。
Action:
- s3:GetObject
- s3:PutObject
どんなイベントが飛んでくるのか調べる
aws-lambda-goのライブラリを使っているのですが、S3Eventと違ってEventBridgeEventは、以下のようなstruct定義になっています。
Detailフィールドにイベントの詳細が入っているのですが、json.RawMessage
となっておりどんなイベントが来るのか分かりませんでした…。
EventBridgeサンドボックスを使うと、サンプルイベントを確認することが出来ます。
今回必要なのはS3のObject Created
イベントです。
最初はそれを知らなかったので、以下のような形でLambdaで受けたイベントを出力して実際に見ていました。
func main() {
lambda.Start(Handler)
}
func Handler(ctx context.Context, event events.CloudWatchEvent) (err error) {
j, _ := json.Marshal(event)
logger.Info(string(j))
return nil
}
その結果、サンプルイベントと同様の形式のイベントが返ってくることを確認できました。
取得したイベント
{
"version": "0",
"id": "********-****-****-****-************",
"detail-type": "Object Created",
"source": "aws.s3",
"account": "************",
"time": "2024-06-17T13:30:26Z",
"region": "ap-northeast-1",
"resources": [
"arn:aws:s3:::example-bucket.dev.example.com"
],
"detail": {
"version": "0",
"bucket": {
"name": "example-bucket.dev.example.com"
},
"object": {
"key": "uploads/videos/sample-video.mp4",
"size": 1234567,
"etag": "********************************",
"sequencer": "******************"
},
"request-id": "************",
"requester": "************",
"source-ip-address": "**********",
"reason": "PutObject"
}
}
S3イベントをフィルタしたい
どんなイベントが飛んでくるか分かったので、特定のバケットの特定のオブジェクトキーのイベントだけを対象とするようにフィルタする設定を行いたいです。
EventBridgeサンドボックスを使うと、イベントパターンをテストしながら構築することができます。
イベントパターンは以下のような形でフィルタすることが出来ました。
イベントパターン
{
"detail-type": ["Object Created"],
"source": ["aws.s3"],
"detail": {
"bucket": {
"name": ["example-bucket.dev.example.com"]
},
"object": {
"key": [{
"wildcard": "uploads/videos/*.mp4"
}]
}
}
}
ビルド用のMakefileを書く必要がある
以前はLambdaのランタイムとしてgo1.x
が提供されており、単純にsam biuild
することでビルド出来ていました。
今はAmazon Linux2ベースのprovided.al2023
上で動かすことが推奨されているようで、カスタムランタイムを構築する必要があるそうです。
具体的には、Makefileに、build-{関数のリソース名}
となるコマンドを作ってあげます。
build-VideoThumbnailGeneratorFunction:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s" -trimpath -o bootstrap .
cp ./bootstrap $(ARTIFACTS_DIR)/.
FFmpegをLambdaにデプロイする
Dockerイメージを作るか、zipパッケージを作成する方法がありますが、後者でやることにしました。
以下にFFmpegのバイナリがあるので、ダウンロードしてzipにパッケージします。
実際は、バイナリをGitリポジトリに入れたくないので、S3に置いておいてsam build
時にダウンロードしてパッケージするようにしました。
Makefileの記述は以下のようにgoのビルド後にバイナリをパッケージするように記述を追加しました。
build-VideoThumbnailGeneratorFunction:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s" -trimpath -o bootstrap .
cp ./bootstrap $(ARTIFACTS_DIR)/.
+ aws s3 cp s3://example-bucket.com/ffmpeg $(ARTIFACTS_DIR)/.
当然ですが、バイナリは、LambdaのRuntimeの種類に合わせて選択してください。
僕は、arm64を使わないといけないのに間違えてamd64を指定してしまっていることに気づかず時間を無駄にしました…。
Lambdaのリトライ処理や失敗時の処理など [追記:2024/7/10]
EventBridgeなどから非同期呼び出しされた場合、Lambdaは、デフォルトで2回リトライしてくれます。
イベントがすべての試行に失敗した場合、SQSキューかSNSトピックに送信するようにデッドレターキューを設定することが出来ます。
template.yamlの設定としては、Lambdaのリソースの設定にDeadLetterQueueの設定を追加します。
DeadLetterQueue:
Type: SNS
TargetArn: !Ref SNSTopicArn
また、SNSをPublishするための権限が必要です。
Action:
- sns:Publish
最終的な成果物
色々躓きましたが、最終的な成果物は大きく以下の3つです。
- Lambdaで動かすgoのコード
- ビルド用のmakefile
- デプロイ用のtemplate.yaml
goのコードは割愛するとして、makefileとtemplate.yamlは以下のようになりました。
makefile
build-VideoThumbnailGeneratorFunction:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s" -trimpath -o bootstrap .
cp ./bootstrap $(ARTIFACTS_DIR)/.
aws s3 cp s3://example-bucket.com/ffmpeg $(ARTIFACTS_DIR)/.
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
SNSTopicName:
Type: String
Resources:
VideoThumbnailGeneratorFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${AWS::StackName}
Handler: bootstrap
Runtime: provided.al2023
Architectures: [arm64]
CodeUri: .
Role: !GetAtt VideoThumbnailGeneratorRole.Arn
DeadLetterQueue:
Type: SNS
TargetArn: !Sub 'arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${SNSTopicName}'
VideoThumbnailGeneratorRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}Role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaS3SNSAndLogsAccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub "arn:aws:s3:::example-bucket.dev.example.com/*"
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- Effect: Allow
Action:
- sns:Publish
Resource: !Sub 'arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${SNSTopicName}'
VideoThumbnailGeneratorEventRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${AWS::StackName}Role
Description: Trigger VideoThumbnailGeneratorFunction when a new video is uploaded to S3
State: ENABLED
EventPattern:
detail-type:
- Object Created
source:
- aws.s3
detail:
bucket:
name:
- example-bucket.dev.example.com
object:
key:
- wildcard: "uploads/videos/*.mp4"
Targets:
- Arn: !GetAtt VideoThumbnailGeneratorFunction.Arn
Id: VideoThumbnailGeneratorFunctionTarget
PermissionForEventsToInvokeLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref VideoThumbnailGeneratorFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt VideoThumbnailGeneratorEventRule.Arn
まとめ
今回はS3にアップロードした動画のサムネイル抽出をするためのLambdaをSAMで実装しました。
AWSのアップデートが早くて数年前の知識では全然足りなかったり、調べても記事の情報が古くなっていることが多くて色々と躓きましたが、今回はキャッチアップする良い機会になりました。
Discussion