SendGridとApexとAWS Lambdaでメール送信

SendGridとApexとAWS Lambdaでメール送信

SendGridテクニカルサポートエンジニアの津田です。ここ数年、FaaS (Function as a Service) の話題をよく耳にします。AWS LambdaGoogle Cloud FunctionsMicrosoft Azure FunctionsIBM OpenWhiskなど、各社が FaaS の環境を発表しています。最近では、イベント駆動型のコード実行をつなぎ合わせて、複雑な機能を実現する事例も、数多く目にするようになりました。実は当社でも FaaS を活用しています。SendGrid の新規会員登録にお申し込みいただくと、Whoisの確認や法人・個人の判別、郵便番号・住所の整合性など、ご登録内容の確認を行うファンクションが実行される仕組みになっています。

本日は、Goの公式ライブラリ、sendgrid/sendgrid-go、を用いて実装したファンクションをAWS Lambdaにデプロイして、メール送信(+α)してみたいと思います。

以下では、

を利用します。日常的に AWS をご利用の方であれば、約10分(0.2米ドル程度)でお試しいただける内容です。

準備

現在のところ、AWS Lambdaの対応言語は、Node.js (JavaScript)、Python、Java (Java 8 互換)、C# で、Goのコードのままではデプロイできません。ただ、Goをビルドすれば各種環境下で動作するシングルバイナリを生成することができるので、デプロイパッケージにこのバイナリを格納してNode.js のコードから実行する、といった方法でファンクションを作成することが可能です。

今回は、さらに簡単にAWS Lambda へのデプロイの操作を実行できるApexを利用します。例としてAmazon S3上に画像をアップロードすると自動的にメール送信するというシナリオにしてみます。

動作確認を行った環境は、

です。

AWS の認証情報

まず、AWS のアクセス認証情報を環境変数に設定します。 AWS コマンドラインインターフェイスをご利用の場合は、設定済のアクセス認証情報を利用できるので、新たに環境変数を指定する必要はありません。

  • AWS_ACCESS_KEY_ID (AWS アクセスキーID)
  • AWS_SECRET_ACCESS_KEY (AWS シークレットアクセスキー)
  • AWS_REGION (AWS リージョン)

プロジェクトの作成

ドキュメントに従い、以下のコマンドを実行してApex をインストールします。Apexのダウンロードサイトからバイナリをダウンロードしていただいても構いません。

curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

そして、プロジェクト用ディレクトリを新規作成して、プロジェクトの初期化を行います。 ここでは ディレクトリ名をsendgrid-lambda-sampleとしています。

$ mkdir sendgrid-lambda-sample
$ cd sendgrid-lambda-sample
$ apex init

初期化の途中で、プロジェクトの名前を入力してください。以降、プロジェクト名がSendGridLambdaSampleの場合を仮定して進みます。すると、自動的に

  • AWS IAM ロール (SendGridLambdaSample_lambda_function)
  • AWS IAM ポリシー (SendGridLambdaSample_lambda_logs)

が生成されます。helloというサンプルファンクションも生成されますので、デプロイと動作確認をします。(AWSコマンドラインインターフェイスをご利用の場合は、–profile <プロファイル名>でアクセス認証情報とリージョンを指定してください)

$ apex --region ap-northeast-1 deploy
$ apex invoke hello --dry-run

{"hello":"world"}

これにより、SendGridLambdaSample_helloというファンクションが生成されます。

最後に、.zipのデプロイパッケージにはバイナリのみ格納し、ソースコード自体は除外する目的で、プロジェクトディレクトリsendgrid-lambda-sampleに以下のような内容のファイル.apexignoreを用意します。

*.go

以上で、Apex の準備が完了しました。

ファンクション: メール送信

さて次は、Go言語向けのSendGrid公式ライブラリを用いて、 メールを送信する機能を実装してみます。SendGridのAPIキーを予め取得しておいてください。

  • SENDGRID_API_KEY:SendGrid のAPI Key(Mail SendへのFULL ACCESSパーミッションが必要)

以下の関数では、SendGridのWeb API v3を用いてメール送信を行います。ライブラリを用いると、メール本文やヘッダ、メタデータや添付ファイル等、SendGridに送るリクエストの内容を、簡単に指定できます。

functionsディレクトリにsendというディレクトリを作成します。下記のコードをmain.goとしてsendディレクトリ配下に格納します。 (紙面の都合上、大部分のエラー処理を省略しています)

package main

import (
    "encoding/base64"
    "encoding/json"
    "io/ioutil"
    "os"

    apex "github.com/apex/go-apex"
    apexs3 "github.com/apex/go-apex/s3"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/sendgrid/sendgrid-go"
    "github.com/sendgrid/sendgrid-go/helpers/mail"
)

func main() {
    apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
        // イベント内容を取得
        var msg apexs3.Event
        if err := json.Unmarshal(event, &msg); err != nil {
            return nil, err
        }

        // メールオブジェクト
        m := mail.NewV3Mail()

        // 送信元
        m.SetFrom(mail.NewEmail("息子", "yourson@example.com"))
        
        // 件名
        m.Subject = "新しい写真だよ"

        // メール本文
        m.AddContent(
            mail.NewContent("text/plain", "孫の写真を送るよ。感想を聞かせてね。"),
            mail.NewContent("text/html", "孫の写真を送るよ。感想を聞かせてね。"),
        )

        // パーソナライゼーション (宛先)
        p := mail.NewPersonalization()
        p.AddTos(
            mail.NewEmail("おじいちゃん", "grampa@example.com"),
            mail.NewEmail("おばあちゃん", "gramma@example.com"),
        )
        m.AddPersonalizations(p)

        // S3のファイルの取得
        region := msg.Records[0].AWSRegion
        cred := credentials.NewEnvCredentials()
        svc := s3.New(
            session.New(),
            aws.NewConfig().WithCredentials(cred).WithRegion(region),
        )
        out, _ := svc.GetObject(&s3.GetObjectInput{
            Bucket: aws.String(msg.Records[0].S3.Bucket.Name),
            Key:    aws.String(msg.Records[0].S3.Object.Key),
        })
        defer out.Body.Close()
        data, _ := ioutil.ReadAll(out.Body)

        // 添付ファイル
        a := mail.NewAttachment()
        a.SetContent(base64.StdEncoding.EncodeToString(data))
        a.SetFilename(msg.Records[0].S3.Object.Key)
        m.AddAttachment(a)

        // リクエスト
        apiKey := os.Getenv("SENDGRID_API_KEY")
        req := sendgrid.GetRequest(apiKey, "/v3/mail/send", "https://api.sendgrid.com")
        req.Method = "POST"
        req.Body = mail.GetRequestBody(m)

        // レスポンス
        res, _ := sendgrid.API(req)
        return res.Body, nil
    })
}

sendディレクトリにfunction.jsonファイルを作成して、 SendGridのAPIキーを記入すると、AWS Lambda上の環境変数が自動的に設定されます。

{
  "environment": {
    "SENDGRID_API_KEY": "XXXXXXXXXXXXXXXXXXXXXX"
  }
}

この状態でデプロイすると、AWS Lambda上にSendGridLambdaSample_send というファンクションができあがります。

$ apex --region ap-northeast-1 deploy

トリガーの設定

Amazon S3 にファイルが格納されるとAWS Lambda に通知するように設定を行います。まず画像を格納するバケットを作成します。ここでは、sendgridlambdasample という名前にします。

$ aws s3api create-bucket \
    --bucket sendgridlambdasample \
    ---region ap-northeast-1 \
    ---create-bucket-configuration LocationConstraint=ap-northeast-1
{
    "Location": "/sendgridattachments"
}

S3のバケットsendgridlambdasampleのイベントをトリガーとして、 ファンクションSendGridLambdaSample_sendを実行できるように設定します。AWS コンソール上では、

  • Lambda > Functions > SendGridSample_send

と進み、「Triggers」タブを選択して「Add trigger」から設定します。

  • Bucket: sendgridlambdasample
  • Event type: Object Created (All) もしくは Put
  • Prefix: なし
  • Suffix: jpg

Trigger

なお、AWSコマンドラインインターフェイスでは、まずAWS Lambda側に権限を付与し、

$ aws lambda add-permission \
    --function-name "SendGridLambdaSample_send" \
    --statement-id "s3-put-event" \
    ---action "lambda:InvokeFunction" \
    ---principal "s3.amazonaws.com" \
    ---source-arn "arn:aws:s3:::sendgridlambdasample"

そしてS3のバケットに通知イベントを仕込みます。

$ aws s3api put-bucket-notification-configuration \
    --bucket "sendgridlambdasample" \
    ---notification-configuration "LambdaFunctionConfigurations\":[{\"LambdaFunctionArn\":\"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SendGridLambdaSample_send\",\"Events\":[\"s3:ObjectCreated:Put\"],\"Filter\":{\"Key\":{\"FilterRules\":[{\"Name\":\"suffix\",\"Value\":\"jpg\" }]}}}]

上の–notification-configurationの内容は、 AWSコマンドラインインターフェイスのマニュアルを参考にしてください。

メール送信

試しに画像をアップロードすると、

$ aws s3 cp grandson.jpg s3://sendgridlambdasample/grandson.jpg

確かにメールが届きました!

AWS LambdaからSendGridでメール送信

実話: フィーチャーフォンの待受画像に

スマートフォンが爆発的に普及したとはいえ、 現在の日本では2割程の方がフィーチャーフォンを継続利用されています。 老若男女を問わず、スマートフォンの利用に二の足を踏む方も一定数いらっしゃいます。 先ほどの画像は 3.5 MBでしたが、 昨今のスマートフォンで撮影した写真はファイルサイズが大きすぎて、 フィーチャーフォンでは保存はおろか閲覧も困難です。

ですから以下のようなリクエストがあるのも頷けます:

毎日使っている携帯だからこそ、待受画面には孫の写真を設定したい。できれば、ほぼリアルタイムで写真を送ってほしいのだけど。

どんなモバイル端末であっても、たいていの場合、メールのやりとりは可能です。ですから、既にデプロイしているメール送信用のファンクションに加えて、 写真をリサイズする追加ファンクションをデプロイすれば、リクエストに答えられそうです。 (余談ですが、企業やデバイスの垣根を越えて、 ここまで世の中に深く浸透しているメールのプロトコルは奇跡的に思えます。) バケット(例えば、sendgridlambdasampleoriginal)へのアップロードをトリガーにして、リサイズした写真を先ほどのバケット(sendgridlambdasample)に保存してみましょう。 (紙面の都合上、大部分のエラー処理を省略しています)

package main

import(
    ...
    apexs3 "github.com/apex/go-apex/s3"
    "github.com/nfnt/resize"
)

func main() {
    apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
        // イベントの内容を取得
        var msg apexs3.Event
        if err := json.Unmarshal(event, &msg); err != nil {
            return nil, err
        }

        // S3のファイルを取得
        region := msg.Records[0].AWSRegion
        cred := credentials.NewEnvCredentials()
        bucket := msg.Records[0].S3.Bucket.Name
        key := msg.Records[0].S3.Object.Key
        svc := s3.New(
            session.New(),
            aws.NewConfig().WithCredentials(cred).WithRegion(region),
        )
        out, _ := svc.GetObject(&s3.GetObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        })
        defer out.Body.Close()

        // 画像をデコード
        img, _ := jpeg.Decode(out.Body)

        // リサイズ
        resizedImg := resize.Resize(480, 0, img, resize.Lanczos3)

        // エンコードした画像を保存
        file, _ := os.Create("/tmp/" + key)
        defer file.Close()

        if err := jpeg.Encode(file, resizedImg, nil); err != nil {
            return nil, err
        }
        post, _ := svc.PutObject(&s3.PutObjectInput{
            Bucket: aws.String("sendgridlambdasample"),
            Key:    aws.String(key),
            Body:   file,
        })

        return post.String(), nil
    })
}

画像サイズの縮小には、nfnt/resizeを利用しました。 画像をアップロードしてみると、

$ aws s3 cp grandson.jpg s3://sendgridlambdasampleoriginal/grandson.jpg

サイズの小さな写真が届きました!

AWS LambdaからSendGridでメール送信 +alpha

まとめ

本日は、SendGridの公式ライブラリの利用例として、FaaS 上のメール送信ツール(+α)を作ってみました。自前でサーバを用意することなく、気軽にそして素早くお試しいただけるのではないかと思います。

SendGridは大規模なメール配信を確実に実現する様々な機能をAPIで提供しています。 また、各種言語のAPIライブラリも公開しています。 SendGrid のAPIをたたいてみたい方は、月間12,000通の無料プランからお試しください。 そして、みなさんもぜひ独自の便利ツールを作ってください。