本記事はFusic Advent Calendar 2020の3日目の記事です。
普段の仕事ではもっぱらPHPばかりですが、最近はGoの勉強を楽しんでいます。
弊社では最近週一で有志のメンバーでGo+serverlessの勉強会を行っておりまして、今回はその課題で作成した内容の備忘録です。Serverless Frameworkを使用しています。
https://www.serverless.com/
環境
Serverless Framework: 2.14.0
aws-cli(動作確認用): 2.1.4
構成
色々と省略していますが、以下のような構成でSQSに来たメッセージをLambdaで取得してSESでメール通知を行うことを想定します。そして、Goを使いたいという理由でLambdaはGoで記述します。
本来、API Gateway→SQS→Lambda→SES/SNS/DynamoDBみたいなものを作ろうとしているのですが、それの一部を切り抜いている形です。エラー処理とかはあまり考慮せずに書いています。
※API Gateway→SQSの構成も後日追記しました。
準備
Serverless Frameworkの導入等は省略しまして、まずはテンプレートを作成します。
serverless create -t aws-go
helloディレクトリとworldディレクトリは不要なので消して、以下のようなディレクトリ構成にします。
.
├── Makefile
├── serverless.yml
└── sqs
└── main.go
実装
SQS→Lambda
ひとまずSQSに入ってきたデータをLambdaで取得出来るか確認します。
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func Handler(ctx context.Context, sqsEvent events.SQSEvent) error {
for _, message := range sqsEvent.Records {
fmt.Printf("The message %s for event source %s = %s \n", message.MessageId, message.EventSource, message.Body)
}
return nil
}
func main() {
lambda.Start(Handler)
}
severless.ymlを記述します。accountId
の部分は自分のアカウントのものに置き換えが必要です。
service: test-sqs-ses-go
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
package:
exclude:
- ./**
include:
- ./bin/**
functions:
sqs:
name: go-queue-worker
handler: bin/sqs
timeout: 30
memorySize: 1024
reservedConcurrency: 1
events:
- sqs:
arn: arn:aws:sqs:ap-northeast-1:{accountId}:go-queue
batchSize: 1
resources:
Resources:
MyQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: "go-queue"
Goのbuild
serverless.ymlのhandlerで指定したパスに実行ファイルを配置します。
GOOS=linux go build -o bin/sqs sqs/main.go
確認
デプロイ
設定したLambdaとSQSをデプロイします。もし、デプロイ時にエラーになる場合はリージョンやロールの設定を確認してみてください。
sls deploy -v
SQSにデータ送信
aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/{accountId}/go-queue --message-body "test"
Lambdaのデータ処理確認
serverless logs -f sqs
START RequestId: 39c95002-76ee-519c-9a9a-08421fc3db82 Version: $LATEST
The message 5f78d4b3-2228-4324-a780-97ce6225a4b5 for event source aws:sqs = test
END RequestId: 39c95002-76ee-519c-9a9a-08421fc3db82
REPORT RequestId: 39c95002-76ee-519c-9a9a-08421fc3db82 Duration: 0.99 ms Billed Duration: 1 ms Memory Size: 1024 MB Max Memory Used: 34 MB Init Duration: 64.94 ms
大丈夫そうですね。
コンソールからLambdaを確認してみると、ちゃんとデプロイされていて、SQSが紐付いているのが分かります。
Lambda→SES
※SESの事前設定は省略
GoのSDKを使って、LambdaでSQSから受け取ったメッセージをそのままメールの本文として送信するように書き換えます。
基本的に公式のサンプル通りの記述です。
送信先と送信元のメールアドレスは適宜変える必要があります。
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
)
const (
Sender = "sender@example.com"
Recipient = "recipient@eample.com"
Subject = "Amazon SES Mail Send Test"
CharSet = "UTF-8"
)
func Handler(ctx context.Context, sqsEvent events.SQSEvent) error {
for _, message := range sqsEvent.Records {
sendSES(message.Body)
fmt.Printf("Send message [%s]! \n", message.Body)
}
return nil
}
func sendSES(message string) {
sess, _ := session.NewSession(&aws.Config{
Region: aws.String("ap-northeast-1")},
)
svc := ses.New(sess)
input := &ses.SendEmailInput{
Destination: &ses.Destination{
CcAddresses: []*string{},
ToAddresses: []*string{
aws.String(Recipient),
},
},
Message: &ses.Message{
Body: &ses.Body{
Text: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(message),
},
},
Subject: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(Subject),
},
},
Source: aws.String(Sender),
}
result, err := svc.SendEmail(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
case ses.ErrCodeMailFromDomainNotVerifiedException:
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
case ses.ErrCodeConfigurationSetDoesNotExistException:
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
func main() {
lambda.Start(Handler)
}
このままだと実行されるLambdaにSESの権限が付いていませんので、serverless.ymlのproviderの箇所を以下に修正します。
provider
name: aws
runtime: go1.x
region: ap-northeast-1
iamRoleStatements:
- Effect: Allow
Action:
- ses:*
Resource: "*"
確認
再びSQS→Lambdaの時と同様にビルド・デプロイを行って、実際メールが届くか確認します。
うまく行けば本文が"test"のメールが届くはずです。
aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/{accountId}/go-queue --message-body "test"
API Gateway→SQS(追記)
serverless-apigateway-service-proxyを利用して、API Gateway→SQSの連携も確認できたので追記します。
まず、プラグインを導入します。
serverless plugin install -n serverless-apigateway-service-proxy
そして、severless.ymlにAPI Gatewayを使う記述を追記するだけです。
queueName
の指定で躓いていたのですが、シンプルな答えでした。
service: test-sqs-ses-go
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
iamRoleStatements:
- Effect: Allow
Action:
- ses:*
Resource: "*"
package:
exclude:
- ./**
include:
- ./bin/**
custom:
apiGatewayServiceProxies:
- sqs:
path: /sqs
method: post
queueName: { 'Fn::GetAtt': ['MyQueue', 'QueueName'] }
functions:
sqs:
name: go-queue-worker
handler: bin/sqs
events:
- sqs:
arn: arn:aws:sqs:ap-northeast-1:{accountId}:go-queue
batchSize: 1
resources:
Resources:
MyQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: "go-queue"
plugins:
- serverless-apigateway-service-proxy
確認
デプロイが終わったら、リクエストを投げてみて動作を確認します。xxxxxxxxx
の部分はデプロイ完了時に表示されるのを見るか、コンソールから確認できます。
curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/sqs -d '{"message": "testtest"}' -H 'Content-Type:application/json'
レスポンスは特に指定していなかったので、MessageId等だけが返ってきます。
また、Lambda側の処理は変えていないので本文がJSONになっていますが、メールも問題なく来ました。
片付け
動作が確認出来て、検証が終わったらリソースは削除しておきます。
sls remove -v
まとめ
Go+serverlessでSQS→Lambda→SESの流れを実装しました。このままだと、まだ機能の一部でしか無いのでアプリケーションとして使用する場合はもっと色々考慮する必要がありそうです。
メール送信の部分を並行処理に出来ればもっと良かったかなと思っています。