Go+serverlessでSQS→Lambda→SES

本記事は[Fusic Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fusic)の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の構成も後日追記しました。**

![aws-diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183878/ca96c680-e4ac-656d-3e86-138f0caf39df.png)


## 準備
Serverless Frameworkの導入等は省略しまして、まずはテンプレートを作成します。

```bash:テンプレートファイル作成
serverless create -t aws-go
```

helloディレクトリとworldディレクトリは不要なので消して、以下のようなディレクトリ構成にします。

```bash:ディレクトリ構成
.
├── Makefile
├── serverless.yml
└── sqs
    └── main.go
```

## 実装
### SQS→Lambda
ひとまずSQSに入ってきたデータをLambdaで取得出来るか確認します。

```go:sqs/main.go
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` の部分は自分のアカウントのものに置き換えが必要です。

```yaml:serverless.yml
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で指定したパスに実行ファイルを配置します。

```bash
GOOS=linux go build -o bin/sqs sqs/main.go
```

#### 確認
##### デプロイ
設定したLambdaとSQSをデプロイします。もし、デプロイ時にエラーになる場合はリージョンやロールの設定を確認してみてください。

```bash
sls deploy -v
```

##### SQSにデータ送信
```bash
aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/{accountId}/go-queue  --message-body "test"
```

##### Lambdaのデータ処理確認
```bash
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が紐付いているのが分かります。

![スクリーンショット 2020-12-03 11.14.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183878/1bb8aec8-613f-a3f8-6ddf-2818797a3ceb.png)

### Lambda→SES
※SESの事前設定は省略
GoのSDKを使って、LambdaでSQSから受け取ったメッセージをそのままメールの本文として送信するように書き換えます。
基本的に公式のサンプル通りの記述です。
送信先と送信元のメールアドレスは適宜変える必要があります。

https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/developer-guide/ses-example-send-email.html

```go:sqs/main.go
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の箇所を以下に修正します。

```yaml:serverless.yml
provider
  name: aws
  runtime: go1.x
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - ses:*
      Resource: "*"
```

#### 確認
再びSQS→Lambdaの時と同様にビルド・デプロイを行って、実際メールが届くか確認します。
うまく行けば本文が"test"のメールが届くはずです。

```bash
aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/{accountId}/go-queue  --message-body "test"
```

OKですね。
![ses-test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183878/cbdea7ee-b1d7-695f-b42d-0df38073497f.png)

### API Gateway→SQS(追記)
[serverless-apigateway-service-proxy](https://github.com/serverless-operations/serverless-apigateway-service-proxy)を利用して、API Gateway→SQSの連携も確認できたので追記します。

まず、プラグインを導入します。

```bash
serverless plugin install -n serverless-apigateway-service-proxy
```

そして、severless.ymlにAPI Gatewayを使う記述を追記するだけです。
`queueName`の指定で躓いていたのですが、シンプルな答えでした。

```yml:serverless.yml
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`の部分はデプロイ完了時に表示されるのを見るか、コンソールから確認できます。

```bash
curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/sqs -d '{"message": "testtest"}' -H 'Content-Type:application/json'
```

レスポンスは特に指定していなかったので、MessageId等だけが返ってきます。
また、Lambda側の処理は変えていないので本文がJSONになっていますが、メールも問題なく来ました。

![スクリーンショット 2020-12-03 23.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183878/370de0ea-3d10-8b5f-98af-f3dd4e22fbb8.png)


## 片付け
動作が確認出来て、検証が終わったらリソースは削除しておきます。

```bash
sls remove -v
```

## まとめ
Go+serverlessでSQS→Lambda→SESの流れを実装しました。このままだと、まだ機能の一部でしか無いのでアプリケーションとして使用する場合はもっと色々考慮する必要がありそうです。
メール送信の部分を並行処理に出来ればもっと良かったかなと思っています。

## 参照
* [サンプル Amazon SQS 関数コード](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-sqs-create-package.html#with-sqs-example-deployment-pkg-go)
* [Serverless Framework(Go) でHello worldしてみる](https://qiita.com/seike460/items/b54a61ec8d07b2be8c87)
* [Serverless Frameworkを使ってsqs+lambda](https://qiita.com/ue_knnk/items/c67b7e3dbb9e3c7e1c4e)