🚀

S3イベントで動くLambda関数をローカル完結で手軽に開発する

2023/12/21に公開

こんにちは!
KANNA の開発のお手伝いをしております、フリーランスエンジニアの len_prog です。

本記事は株式会社アルダグラム Advent Calendar 2023 21日目の記事です。


今年の11月にリリースした新規プロダクト「KANNAレポート」のバックエンドでは、帳票に画像をアップロードできる機能があります。
この機能の開発に関わって、AWS SAM のS3イベントトリガー関数をローカル完結で開発する方法を発見したので、今回はその方法についてハンズオン形式でご紹介します。

前提

  • minio がインストール済みで、http://localhost:9000 に立ち上がっていること
  • AWS SAM CLI がインストールされていること
  • Docker の実行環境があること

依存関係のインストール

AWS Lambda Runtime Interface Emulator をダウンロードして、実行権限を与えます。
このバイナリは、ローカルで関数の実行を行うために必要です。

mkdir -p ~/.aws-lambda-rie && \
    curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \
    chmod +x ~/.aws-lambda-rie/aws-lambda-rie

関数を作成する

動かす関数がないことには動作確認も何もないので、まずは関数を実装します。
なお、完成品を https://github.com/h-tachikawa/s3-trigger-function-example にアップロードしてありますので、必要な場合はこちらをご利用ください。
(こちらを利用する場合、 Docker イメージのビルド まで手順を飛ばすことができます)

まず、$ sam init を実行して、関数を作成します。
Go で実装して、Docker コンテナとして動作するようにセットアップします。

$ sam init

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Data processing
	3 - Hello World Example with Powertools for AWS Lambda
	4 - Multi-step workflow
	5 - Scheduled task
	6 - Standalone function
	7 - Serverless API
	8 - Infrastructure event management
	9 - Lambda Response Streaming
	10 - Serverless Connector Hello World Example
	11 - Multi-step workflow with Connectors
	12 - GraphQLApi Hello World Example
	13 - Full Stack
	14 - Lambda EFS example
	15 - Hello World Example With Powertools for AWS Lambda
	16 - DynamoDB Example
	17 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: n

Which runtime would you like to use?
	1 - aot.dotnet7 (provided.al2)
	2 - dotnet6
	3 - go1.x
	4 - go (provided.al2)
	5 - go (provided.al2023)
	6 - graalvm.java11 (provided.al2)
	7 - graalvm.java17 (provided.al2)
	8 - java17
	9 - java11
	10 - java8.al2
	11 - java8
	12 - nodejs20.x
	13 - nodejs18.x
	14 - nodejs16.x
	15 - nodejs14.x
	16 - python3.9
	17 - python3.8
	18 - python3.7
	19 - python3.11
	20 - python3.10
	21 - ruby3.2
	22 - ruby2.7
	23 - rust (provided.al2)
	24 - rust (provided.al2023)
Runtime: 4

What package type would you like to use?
	1 - Zip
	2 - Image
Package type: 2

Based on your selections, the only dependency manager available is mod.
We will proceed copying the template using mod.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: n

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: n

Project name [sam-app]: s3-trigger-function-example

    -----------------------
    Generating application:
    -----------------------
    Name: s3-trigger-function-example
    Base Image: amazon/go-provided.al2-base
    Architectures: x86_64
    Dependency Manager: mod
    Output Directory: .
    Configuration file: s3-trigger-function-example/samconfig.toml

    Next steps can be found in the README file at s3-trigger-function-example/README.md


Commands you can use next
=========================
[*] Create pipeline: cd s3-trigger-function-example && sam pipeline init --bootstrap
[*] Validate SAM template: cd s3-trigger-function-example && sam validate
[*] Test Function in the Cloud: cd s3-trigger-function-example && sam sync --stack-name {stack-name} --watch

$ cd s3-trigger-function-example

関数の実装

AWS SDK for Go のインストール

minio にオブジェクトをアップロードするために、AWS SDK をインストールします。

$ cd hello-world
$ go get github.com/aws/aws-sdk-go-v2/aws
$ go get github.com/aws/aws-sdk-go-v2/service/s3
$ go get github.com/aws/aws-sdk-go-v2/config

S3イベントを再現したjsonを作成する

$ sam local generate-event を実行して、S3 にオブジェクトを PUT するイベントを再現した json を作成します。(詳しくは公式ドキュメントをご参照ください)
こちらは、関数のローカルでの動作確認に必要となります。

$ sam local generate-event s3 put --bucket test-bucket --key /test.jpg > s3_image_put_event.json

./hello-world/main.go を編集する

S3のイベントを受け取って、イベントの発火元になった画像をリネームして minio にアップロードしなおす簡単なプログラムを書きます。
以下のコードをコピーして、./hello-world/main.go にペーストしてください。

./hello-world/main.go
package main

import (
	"bytes"
	"context"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"log"
	"os"
)

func handler(ctx context.Context, s3Event events.S3Event) error {
	endpoint := os.Getenv("S3_ENDPOINT")

	if _, err := os.Stat("/tmp"); os.IsNotExist(err) {
		if err := os.Mkdir("/tmp", os.ModePerm); err != nil {
			log.Printf("error when creating tmp directory. %v", err)
			return err
		}
	}

	resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, opts ...interface{}) (aws.Endpoint, error) {
		if service == s3.ServiceID && len(endpoint) > 0 {
			return aws.Endpoint{
				URL:               endpoint,
				HostnameImmutable: true,
			}, nil
		}
		return aws.Endpoint{}, &aws.EndpointNotFoundError{}
	})

	sdkConfig, err := config.LoadDefaultConfig(
		ctx,
		config.WithEndpointResolverWithOptions(resolver),
	)

	if err != nil {
		log.Printf("failed to load default config: %s", err)
		return err
	}
	s3Client := s3.NewFromConfig(sdkConfig)
	record := s3Event.Records[0]
	bucket := record.S3.Bucket.Name
	key := record.S3.Object.Key
	image, err := s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: &bucket,
		Key:    &key,
	})

	if err != nil {
		return err
	}

	targetFilePath := "/tmp/test-copied.jpeg"
	file, err := os.Create(targetFilePath)
	if err != nil {
		return err
	}
	defer file.Close()
	buf := new(bytes.Buffer)
	buf.ReadFrom(image.Body)

	_, err = file.Write(buf.Bytes())
	newKey := "/test-copied.jpeg"

	_, err = file.Seek(0, 0)
	if err != nil {
		return err
	}

	_, err = s3Client.PutObject(ctx, &s3.PutObjectInput{
		Bucket: &bucket,
		Key:    &newKey,
		Body:   file,
	})

	if err != nil {
		return err
	}

	return nil
}

func main() {
	lambda.Start(handler)
}

Docker イメージのビルド

実装した関数を Docker イメージとしてビルドします。

$ docker build --platform linux/amd64 -t my-image:test ./hello-world

Docker コンテナを起動

Docker イメージを元に、Dockerコンテナを起動します。
S3_ENDPOINT='http://host.docker.internal:9000'を環境変数として渡すことで、コンテナの中からホストのマシンの9000番ポート(minio)を見に行くことができます。

$ docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 11111:8080 \
--entrypoint /aws-lambda/aws-lambda-rie \
--env AWS_ACCESS_KEY_ID='YOUR_MINIO_ACCESS_KEY' \
--env AWS_SECRET_ACCESS_KEY='MINIO_SECRET_ACCESS_KEY' \
--env AWS_REGION='ap-northeast-1' \
--env S3_ENDPOINT='http://host.docker.internal:9000' \
my-image:test ./lambda-handler

minio に変換前の画像をアップロードする

今回動かすプログラムは、S3から画像を読み取って名前を変更してアップロードしなおすものなので、http://localhost:9000 から minio のコンソールにログインし、変換元の画像を test-bucket/test.jpeg にアップロードしてください。

Lambda 関数を実行

events ディレクトリ以下に、lambda 関数の実行時に渡すイベントを格納しています。
この json を関数に渡すことで、ローカルでS3イベントトリガーの関数を実行できます。

$ curl -X POST -H 'Content-Type: application/json' \                
-d @./s3_image_put_event.json  \
"http://localhost:11111/2015-03-31/functions/function/invocations"

実行結果を確認する

minio の test-bucket を確認すると、変換後の画像がアップロードされていることが確認できます!

お疲れ様でした!
これで無事に、ローカル環境完結で Lambda の動作確認ができました。

まとめ

本記事では、AWS SAMを活用してS3イベントに対応するLambda関数をローカル環境で開発する手順について解説しました!
この記事が皆さんの Lambda 関数開発の一助となれば幸いです。

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

余談

実は、もともと $ sam local invoke コマンドを用いてローカルで動作確認ができるようにもなっているのですが、
この方法だと Docker のビルド時に出たエラーがターミナルに表示されなかったり、minio と上手く接続させることができなかったりと、少々厳しい部分がありました。
そのため、今回のやり方を模索して発見したのですが、もし sam local invoke を使う方法で上手くいくやり方をご存知の方は、コメントいただけますと幸いです!

GitHubで編集を提案
アルダグラム Tech Blog

Discussion