🐕

AWSのキャッチアップをしながら、S3にアップロードした動画のサムネイルを抽出するLambdaを作った

2024/07/04に公開

はじめに

こんにちは。株式会社バニッシュ・スタンダードでサーバーサイドエンジニアをやっているhidechaeです。

弊社にはインフラチームがあるので、アプリケーションの開発チームがゴリゴリインフラを触る機会は少ないのですが、一部Lambdaなど実装が絡むところは、サーバーサイドエンジニアがSAMを書いたりしています。

僕個人としては、5年前くらいまではAWSを業務で触っていたりしましたが、ここ数年はインフラチームが作ってくれたものを見ているだけで細かいことは全然キャッチアップできてない状態でした。
そんな中、久々にLambda作っていて色々と詰まったので記録に残しておきます。誰かの役に立つといいな…

やりたいこと

今回やりたかったことは動画のサムネイルを作成です。

実装方針

まず、大きな方針として以下の3案あります。

  1. 動画アップロード時にサーバーサイドでffmpegを使ってサムネイルを作成する
  2. S3に動画をアップロードした後、非同期でLambdaでffmpegを使ってサムネイルを作成する
  3. Elemental MediaConvertを使ってサムネイルを作成する

前提としてサーバーサイドはgoで書かれており、Fargateで動いています。

1に関しては、構成が最もシンプルですが、Dockerイメージのサイズが大幅に大きくなってしまうため、コスト面や起動速度の低下などデメリットが大きいということで却下しました。

3に関しては、Elemental MediaConvertではサムネイルのみの作成ができず、動画の変換処理とセットになってしまうため、今回の要件には合わないということで却下しました。

結果的に2のLambda上でffmpegを動かすパターンで実装することにしました。

アーキテクチャ

サムネイル作成までの流れは以下のようになります。

  1. ClientからS3に動画をアップロードする
  2. S3のイベント通知をEventBridgeに送信する
  3. EventBridgeがLambdaを起動する
  4. Lambdaがサムネイルを作成する
  5. 作成したサムネイルをS3にアップロードする

途中で詰まって試行錯誤したこと

初歩的な躓きが多いのですが一通り記録しておこうと思います。

SAMでは既存のS3バケットにS3イベント通知設定出来ない

当初、S3イベントからLambdaを起動しようと思って作業していました。
作業している中で、SAMで構築する場合、template.yaml内で既存のS3バケットにS3イベントを設定することは出来ず、新規S3バケットを構築する必要があることが分かりました。
そのため、EventBridge経由でLambdaを起動することにしました。

CloudTrailは不要らしい

調べていると、S3 → CloudTrail → EventBridge といった構成のサンプルがよく出てくるなぁと思っていましたが、今はCloudTrail証跡なしでEventBridgeで直接S3イベントを拾えるようになっているようです。

https://aws.amazon.com/jp/blogs/aws/new-use-amazon-s3-event-notifications-with-amazon-eventbridge/?nc1=h_ls

S3のイベントがEventBridgeに飛ばない

S3のバケットのプロパティでEventBridgeへのイベント通知設定をオンにしなければ通知は飛びません。
僕は気づかず結構な時間を無駄にしました…。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html

権限が色々足りなくて動かない

まず、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となっておりどんなイベントが来るのか分かりませんでした…。

https://github.com/aws/aws-lambda-go/blob/main/events/cloudwatch_events.go

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サンドボックスを使うと、イベントパターンをテストしながら構築することができます。

https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-event-pattern-sandbox.html

イベントパターンは以下のような形でフィルタすることが出来ました。

イベントパターン
{
  "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)/.

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/building-custom-runtimes.html

FFmpegをLambdaにデプロイする

Dockerイメージを作るか、zipパッケージを作成する方法がありますが、後者でやることにしました。

以下にFFmpegのバイナリがあるので、ダウンロードしてzipにパッケージします。

https://johnvansickle.com/ffmpeg/

実際は、バイナリを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

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/invocation-async.html

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-deadletterqueue.html

最終的な成果物

色々躓きましたが、最終的な成果物は大きく以下の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