API Gateway + lambda + SESをrubyで書いてみた。

slack等チャットサービスが主流となりつつある今日この頃ですが、何かと使う機会が多いAPI Gateway + lambdaでメール通知が必要な案件がありましたので覚え書き。
ついでに、駆け出しRubyistなものでlambdaもrubyで書いてみました。

AWSでメールといえばSES(Simple Mail Service)ですね。今回はAPIにPOSTされたデータの内容をメール本文に入れて送信してみます。

# 1. lambdaからメール送信してみる

まずはlambda - sesだけでメールの送信テスト。

## SESの設定

2019/3時点で日本リージョンにありませんでした。バージニア北部(us-east1)で使います。

### Email Addressの登録

送信元となるメールアドレスを登録します。登録すると確認メールが飛ぶのでリンクをクリック。
ステータスがverifyになればOK。
![](https://i.imgur.com/zOc6nAB.png)

なお、SESの初期状態はサンドボックスモードで稼働するため、送信先もこちらのEmail Addressに登録されたアドレスのみとなります。
今回の要件では送信先・送信元が同一なので実施しませんでしたが、制限を解除する場合はAWSサポートから申請してください。

## lambdaの設定

まずはテストということで、lambdaのtestからメールを送信できるようにします。

### Roleの作成

先にlambdaからsesをcallできるようにIAM Roleを作っちゃいます。
紐づけるポリシーは以下2つ。可読性のためにcloudwatch(ログ)周りとSES周りで分離しました。

```json:lambda_ses_pol
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        }
    ]
}
```

```json:lambda_ses_pol

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendBounce",
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "*"
        }
    ]
}
```

lambda_use_ses_roleという名前でRoleを作成し、上記ポリシーを紐づけ。

### 関数の作成

関数を作成します。名前は適当にsendmail_ses、ランタイムはいざ、Ruby2.5に。
実行ロールには先ほど作成した lambda_use_ses_roleを選択。DesignerにcloudwatchとSESが追加されればOK。

```ruby:lambda_function.rb
require 'json'
require 'aws-sdk'

def lambda_handler(event:, context:)

  to = "yourmail@example.com"
  from = "yourmail@example.com"
  subject = "Amazon SES test"
  sesregion = "us-east-1"
  encoding = "UTF-8"

  begin

    body = "test mail"
     
    ses = Aws::SES::Client.new(region: sesregion)
    resp = ses.send_email({
      destination: {
        to_addresses: [
          to,
        ],
      },
      message: {
        body: {
          text: {
            charset: encoding,
            data: body,
          },
        },
        subject: {
          charset: encoding,
          data: subject,
        },
      },
      source: from,
    })
    return body
  rescue Aws::SES::Errors::ServiceError => error
    return "Email not sent. Error message: #{error}"
  end
end
```

AWS公式ドキュメントを参考にしています。

## テスト

lambdaからテストを作成、実行。(リクエストデータは関係ないので初期値で)
メールが届いていたら幸せ。順調ですね。

# 2. API化してみる

続いて上記lambda-SES構成をAPI化します。

## API Gatewayの設定

### APIの作成

プロトコルはREST、エンドポイントは外部からのアクセスを想定しているのでリージョンを選択。

![](https://i.imgur.com/eQFZWWf.png)

### リソースの設定

リソース -> アクションからメソッドの作成を選択して任意のものを作成。
今回はwebアクセスで確認したかったのでGETで。

![](https://i.imgur.com/Yd8tVAQ.png)

### テスト

早速テストしてみます。GETメソッドからテストをクリックするとリクエストデータを入力できます。
が、今回は関係ないので空でもOK。

メールが送信されたら幸せです。いい感じ。

### リソースポリシーの設定

公開時にIP制限をかけたかったのでリソースポリシーに以下を追加

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:ap-northeast-1:AWSアカウントID:APIID/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        }
    ]
}
```

APIIDは画面上API名の横に()でくくって書いてあります。許可するIPは任意のものを。

### デプロイ

作成が完了したらAPIをデプロイ(公開)します。
アクションからAPIのデプロイ、ステージは新しいステージを選択し、任意のものを指定します。
![](https://i.imgur.com/JVn3l7Y.png)

完了するとステージが作成され、エンドポイントURLが表示されます。

## テスト

早速ブラウザからエンドポイントにアクセス。(webアクセスではGETが実行されます。)
リソースポリシーで許可したIPから接続することを忘れずに！

テスト時の応答と同じく、
メールが送信されたら幸せです。ハッピー！

# 3. リクエストデータの値を取得してみる

APIが動作してメール送信まで確認出来たので、ちょっと実用的(というか必須ですよね)なことを。
jsonで来たリクエストをlambdaでごにょごにょしてメール本文に加えます。

## API Gatewayの設定

こちら側では「マッピングテンプレート」という機能でリクエストをlambdaに渡す条件、内容の編集を行えます。
どこまでをマッピングテンプレートで処理するか、またはlambda側で処理するかは好みがわかれますが、今回はテストなのでjsonデータをハッシュ化してまるっと渡します。

### マッピングテンプレートの設定

リソースからメソッドを選択し、統合リクエスト(日本語だと変な感じですね。Integrationだと思いますが。)を選択。

マッピングテンプレートで、リクエスト本文のパススルーは推奨設定を選択。(任意のものを選択してください。)
マッピングテンプレートの追加から、コンテンツタイプを入力します。jsonなのでapplication/jsonで。
ソースはこんな感じで全文取得。
![](https://i.imgur.com/bPa9Aka.png)

これでlambda側で値を取得できるようになります。

## lambdaの設定

続いてlambda側、とりあえずメール本文にそのまま値を突っ込みます。

```ruby:lambda_function.rb
require 'json'
require 'aws-sdk'

def lambda_handler(event:, context:)

  to = "yourmail@example.com"
  from = "yourmail@example.com"
  subject = "Amazon SES test"
  sesregion = "us-east-1"
  encoding = "UTF-8"

  begin

    body = event["jsonbody"]["text"]
     
    ses = Aws::SES::Client.new(region: sesregion)
    resp = ses.send_email({
      destination: {
        to_addresses: [
          to,
        ],
      },
      message: {
        body: {
          text: {
            charset: encoding,
            data: body,
          },
        },
        subject: {
          charset: encoding,
          data: subject,
        },
      },
      source: from,
    })
    return body
  rescue Aws::SES::Errors::ServiceError => error
    return "Email not sent. Error message: #{error}"
  end
end

```
リクエストデータはlambda_handlerのeventに格納されています。context等使い方は公式ドキュメントまで。
bodyをちょこっと編集して、リクエストデータの'test'というキーの値を取得します。

## テスト

API Gateway側のテストで、リクエストデータに'test'キーを入れてあげて実施。
メールが届けばすべて完了です。最高！
![](https://i.imgur.com/AtcyTWv.png)

# カスタムドメイン名を設定する

API Gatewayのカスタムドメイン名を使用して、独自ドメインでエンドポイントを提供します。ついでにSSL化も。

※既にRoute53で独自ドメインを管理している前提

## 証明書の取得

使用したいドメインの証明書を取得します。Certificate Managerから利用したいFQDNの証明書を取得してください。(ワイルドカード証明書でも可)

この時、APIがreginalの場合は同じリージョンで証明書を取得する必要があります。

## カスタムドメイン名の登録

API Gateway カスタムドメイン名からFQDNの入力、先ほど取得した証明書を選択すると設定は完了です。(リージョナルを選択)

![](https://i.imgur.com/uGpgCMl.png)
(黒塗りばかりですみません…)

ベースパスマッピングにデプロイ済みのステージを登録するのを忘れずに。

## レコード追加

最後に、Route53にてAレコードの登録を行います。
AレコードのAliasを選択し、上記ターゲットドメイン名を登録します。

以上で設定は完了。登録したカスタムドメイン名でアクセスができたら幸せ。

# まとめ

今回は１から全て作りましたが、lambdaでAPIしてるなかで、ちょっとメールで送りたいなーという時に簡単に実装できるかと思います。

要件にもよりますが外部に公開したり本番運用する場合には、SESの制限にご注意ください。


P.S. AWSの日本語訳版、もうちょっと分かり易いと良いんですけどね。「エッジ最適化」とかなんのこっちゃって感じですし。これからに期待です。(^^)