S3イベントで動くLambda関数をローカル完結で手軽に開発する
こんにちは!
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
にペーストしてください。
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
を使う方法で上手くいくやり方をご存知の方は、コメントいただけますと幸いです!
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion