S3互換ストレージWasabiの内容をSendGridでメール配信してみる

# 概要

こんにちは。Wano株式会社でエンジニアをしている[@kotobuki5991](https://qiita.com/kotobuki5991) です。
この記事はWano Group Advent Calendar20223 20日目の記事です。

弊社のプロダクトで新しく実装中の機能の一部に、<b>S3（等のストレージ）に保存されたデータを読み取りメールを送信する</b>という要件があります。

技術選定の中で、ストレージとしてWasabiを、メール配信サービスとしてSendGridを試しに触ってみたので設定・実装などを紹介します。

# Wasabiとは？


https://wasabi.com/ja/s3-api-compatible-wasabi-cloud-storage

S3完全互換のクラウドストレージで、現在１３のリージョンが使用可能です。
すでにS3を使用しているアプリケーションであれば、修正の必要なく移行することが可能です。
また、S3に対して以下のような利点があります。

- 高速
- 下り料金無料!!!

![wasabiの値段.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/cc648879-99b6-6e6d-5b0f-70dbb00a85d5.gif)

# SendGridとは？


https://sendgrid.kke.co.jp/

クラウド型のメール配信サービスです。
アカウントを作成するだけで即日メールを送信でき、面倒でコストのかかるメールサーバの構築は不要です。
以下のようなメリットがあります。

- 12000通/月まで無料
- 独自ドメインは無料で使用できる。
- Fromのメールアドレスも自由に設定できる。

ただし、送信元を固定IPアドレスにするにはProプラン以上の選択が必要です。（10,000円〜）

# Wasabiの設定からファイルのアップロードまで

まずはWasabiの設定です。

## 1. バケットの作成

wasabiのダッシュボードにログインし、Create Bucketをクリックします。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/6195a55b-97f6-8ecf-67c3-9bf9e1aa1184.png" />


バケットの情報を入力し、Create Bucketを再度クリックします。

<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/1e2fa1e0-2ca5-1982-8277-6deebdaf9c24.png" />


バケットのが完了しました！

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/db2e281a-4ae0-1466-c9f9-e2e3dd04f4c4.png" />


## ユーザーの作成

ダッシュボードのUsersから、ユーザー情報を入力してCreate Userをクリックします。
今回は、S3FullAccessのポリシーを付与しておきます。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/6d418acf-d06b-86a4-8e7d-acddfa4a4511.png" />

作成後、アクセスキーのCSVがダウンロードできるので、忘れずにしておきましょう。
<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/a415eec7-9772-b4e5-96ed-aa96d24672de.png" />

ダウンロードしたCSVに記載されているキーを~/.aws/credentialsに登録します。
以下のようになっていたらOKです。

```
$ cat ~/.aws/credentials
[wasabi] // 任意の名前
aws_access_key_id = XXXXXXXXXXXXXXXXXXX
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## 3. ポリシーの追加
次に先ほど作成したバケットにポリシーを追加します。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/32670e70-3ab2-b2d2-f094-21b5d4df62af.png" />

以下の画面でポリシーを追加します。
<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/051ce32e-b93b-5b27-4064-27725fbec56e.png" />

今回は以下のように追加しました。ユーザーやバケットは自身のものに修正してください。
```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAccessToBucketNamedBucket2",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456123456:user/sample-wasabi-user-1" // 作成したユーザーを指定
      },
      "Action": [
        "s3:ListBucket",
        "s3:GetObject",
        "s3:PutObject",
        "s3:GetObjectAcl",
        "s3:PutObjectAcl",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::sample-wasabi", // 作成したバケットを指定
        "arn:aws:s3:::sample-wasabi/*" // 作成したバケットを指定
      ]
    }
  ]
}
```

## 4. ファイルのアップロード

実際にファイルをアップロードします。
ここで重要なのはエンドポイントのURLです。
ap-northeast-1の場合は、`https://s3.ap-northeast-1.wasabisys.com`となります。
上記を指定しないと、AWSを見に行ってしまいます。

```
$ aws s3 --profile wasabi --endpoint-url https://s3.ap-northeast-1.wasabisys.com cp ./wasabifile1.txt s3://sample-wasabi/hoge.txt 
upload: ./wasabifile1.txt to s3://sample-wasabi/hoge.txt
```

先ほどアップロードしたファイルが確認できました。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/650e7c2f-4a33-8fe7-3c77-18c8cd267569.png" />


# Wasabiからファイルリスト取得〜メール送信の実装例

プロジェクトの構成は以下の通りです。
```
 root/
 　├ aws-util/s3.go
　　　├　sendgrid
 　│     └ main.go
　　　│　    　└ sendgrid.env
 　├ .env
 　├ go.mod
　　　└ go.sum
```

# Wasabiからデータを読み取る実装

今回は、アップロードされたファイル一覧を確認したいため、ListObjectsメソッドを実装しています。

```s3.go
package awsutil

import (
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/joho/godotenv"
)

type S3Client interface {
	ListObjects(bucketName string) ([]*s3.Object, error)
}
type implS3Client struct {
	S3Client *s3.S3
}

func NewS3Client() (i *implS3Client, err error) {

	err = godotenv.Load("../.env")
	
	if err != nil {
		fmt.Printf("読み込み出来ませんでした: %v", err)
		return nil, err
	}
	profile := os.Getenv("WASABI_AWS_PROFILE")
	region := os.Getenv("WASABI_AWS_REGION")

	// create a configuration for profile
	s3Config := aws.Config{
		Endpoint:         aws.String("https://s3.ap-northeast-1.wasabisys.com"),
		Region:           aws.String(region),
		S3ForcePathStyle: aws.Bool(true),
	}

	// create a new session using the config above and profile
	goSession, err := session.NewSessionWithOptions(session.Options{
		Config:  s3Config,
		Profile: profile,
	})

	// check if the session was created correctly.
	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	// create a s3 client session
	s3Client := s3.New(goSession)

	i = &implS3Client{
		S3Client: s3Client,
	}

	return i, err
}

func (s *implS3Client) ListObjects() ([]*s3.Object, error) {
	bucketName := os.Getenv("WASABI_AWS_BUCKET")
	result, err := s.S3Client.ListObjectsV2(&s3.ListObjectsV2Input{
		Bucket: aws.String(bucketName),
	})
	var contents []*s3.Object
	if err != nil {
		log.Printf("Couldn't list objects in bucket %v. Here's why: %v\n", bucketName, err)
	} else {
		contents = result.Contents
	}
	return contents, err
}

```

# メール送信までの実装

次に、S3の情報を読み取ってメールを送信する実装です。
SendGridのアカウントは持っている前提で説明します。

マイページのダッシュボードから、Email API >> Integration Guideへ進みます。

今回はWeb APIを選択します。
<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/203f15ca-d924-0b86-b0de-49902da3f1ac.png" />

Goを選択します。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/a780f8b1-92fd-2bed-2638-476847c6cbd2.png" />

画面の指示の通りにコマンドを実行します。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/4cb673d7-d7e2-1e20-12fb-73f293bc2380.png" />

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/19e991d5-c4df-96c5-9698-396cc6cf7808.png" />



```main.go
// using SendGrid's Go Library
// https://github.com/sendgrid/sendgrid-go
package main

import (
	"fmt"
	"log"
	"os"

	awsutil "github.com/fukuju-t/mail-project/aws-util"
	"github.com/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
)

const confirmMessage = `
ファイル数: %d件

%s
`


func main() {

	s3, err := awsutil.NewS3Client()
	if err != nil {
		log.Fatal(err)
	}
	objects, err := s3.ListObjects()
	if err != nil {
		log.Fatal(err)
	}
	objectsCount := len(objects)
	objectNames := ""
	for _, v := range objects {
		objectNames = "・" + objectNames + *v.Key + "\n"
	}

	from := mail.NewEmail("Wasabi テストメール", "test@example.com")
	subject := "Wasabiのファイルリストです"
	to := mail.NewEmail("hoge", "yourmailaddress@test.com") // 送信先（今回は自身の）メールアドレス

	plainTextContent := fmt.Sprintf(confirmMessage, objectsCount, objectNames)
	htmlContent := ""
	message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
	client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))
	response, err := client.Send(message)
	if err != nil {
		log.Println(err)
	} else {
		fmt.Println(response.StatusCode)
		fmt.Println(response.Body)
		fmt.Println(response.Headers)
	}
}

```

## メール送信！

成功すると以下のような出力となります。
```
$ go run main.go
202

map[Access-Control-Allow-Headers:[Authorization, Content-Type, On-behalf-of, 
x-sg-elas-acl] Access-Control-Allow-Methods:[POST] Access-Control-Allow-Origin:
// 以下省略
```

以下のようなメールが届きます。

<img width=700 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/0f7d1224-6f2f-c766-fb6e-e2db59d4e929.png" />


# まとめ

今回はWasabiとSendGridを試してみました。

WasabiはバケットからのDLなど下り通信が多いほどS3よりお得に使えそうです。
また、SendGridは実装が非常に楽で無料なのがいいですね。

<br>

現在、Wanoグループ / TuneCore Japan では人材募集をしています。興味のある方は下記を参照してください。
Wano | [Wano Group JOBS](https://group.wano.co.jp/jobs/)
TuneCore Japan | [TuneCore Japan JOBS](https://www.tunecore.co.jp/jobs)