[AWS] ゼロから始めてLambdaでメールを送信するまで


:::note info
このドキュメントについて
ゼロからスタートして、AWS Lambdaを使って、あるURLを叩くとHTMLを吐き出しつつメールを送信するプログラムを実装します。
:::

# 概要と準備

Lambdaというサーバレスな仕組みを使って、これまでPHPで作っていたようなWebアプリの実装をするとします。PHPで作られたWebアプリの基本構成は「Web3層構造」と呼ばれるものです。基本的には以下の機能が求められます

1. あるURLを叩くとプログラムが起動する
1. 必要な情報をデータベースに問い合わせる
1. 必要に応じてユーザにメール送信する
1. 結果をHTMLで返し、ユーザはブラウザでそれを表示する

これらの機能がLambdaで実装できるなら、WebサーバとPHPで作られていたシステムをLambdaで代替する事ができます。

で、いったんデータベースの事は忘れましょう。『URLを叩くとプログラムが動き、その中でメール送信ができ、また結果のHTMLを吐き出して、URLを叩いたユーザのWebブラウザ上に表示する』というここを実装してみます。

ゼロからスタートと書きましたがAWSのアカウントはもっているという所をゼロとします。つまり、**AdministratorAccess**ポリシーが割り当てられたIAMアカウントがある状態からスタートします。リージョンは、テストのためどこでも良いのですが、とりあえず、フランクフルト(**eu-central-1**)とします。ミュンヘンの方が好きな街ですが・・・。

使うツールは次の四つです。

* **IAM**: Lambdaがメール送信できるようなポリシーやロールを作る
* **API Gateway**: URLを叩くとイベントを発生させる仕組み
* **Lambda**: イベントに応じてプログラムが実行する仕組み
* **SES (Amazon Simple Email Service)**: メール送信のサービス

# ポリシー作成

> この節で使うツール「[IAM > ポリシー](https://us-east-1.console.aws.amazon.com/iamv2/home?region=eu-central-1#/policies)」

* [ポリシー作成]ボタンを押す

    『ポリシーエディタ』という画面がでてきます。『サービスを選択』の下にある検索のテキストエリアに『**SES**』と入れてみましょう。関連のサービスが出てきます。その中から『**SES v2**』をクリックします。

さらにSES v2に定義されているアクションから許可するアクションを選びます。テスト用途ですので『**すべての SES v2 アクション (ses:*)**』をチェックするのが簡単でしょう。あるいは送信だけですので『**書き込み**→**SendEmail**』を選びます。

『**リソース**』の欄のラジオボタンが『**特定**』になっていますが『**すべて**』にしましょう。赤いWarningがでていますが、ページ下部の『**次へ**』を押してください。

ポリシー名を求められますのでわかりやすい名前を付けてください。とりあえず、私は『TEST_SES_POLICY』としました。ページ最下部まで下がり『**ポリシーの作成**』を押しましょう。ページ上部に『ポリシー TEST_SES_POLICY が作成されました。』と出てきたら成功です。

次はロールの作成です。

# ロール作成

> この節で使うツール「[IAM > ロール](https://us-east-1.console.aws.amazon.com/iamv2/home?region=eu-central-1#/roles)」

ロールにて、Lambdaに『SESで送信できるポリシー』を割り当てます。ではまず『**ロールの作成**」をクリックしましょう。

LambdaというAWSのサービスに対してロールを割り当てますので『**AWS のサービス**』を選びます。『**サービスまたはユースケース**』というドロップダウンメニューから『**Lambda**』を選び、『**次へ**』をクリックします。

次の画面でLambdaに対して割り当てるポリシーを選びます。長大な一覧が出てくるのですが、検索欄に先ほど指定したポリシーの名前(**TEST_SES_POLICY**)を入れると、それだけが出てきます。ポリシー名の左側のチェックボックスをチェックし、『**次へ**』進みましょう。

ロール名を指定する画面がでてくるので、わかりやすい名前を付けてください。とりあえず、私は『TEST_SES_ROLE』としました。ページ下部の『**ロールを作成**』をクリックすれば完了です。

これで、Lambdaからメール送信する操作に対する権限周りのお膳立てが完了しました。次にSESを設定してきましょう。

# SESドメイン認証

> この節で使うツール「[Amazon SES > 検証済みID](https://eu-central-1.console.aws.amazon.com/ses/home?region=eu-central-1#/verified-identities)」

最近、個人でメールサーバやメーリングリストサーバを立てた事がある人は、その大変さにだいぶ苦労したのではないでしょうか。かつてはWebサーバと同じくらい気楽に立てられましたが、スパム防止の仕組みが色々と導入されるようになり、SPFだのDKIMだのDMARCだの、それらをつく合わせたSMTPサーバを建てるのは本当に設定が大変です。

Amazon SESを使って送信する事により、DMARCに準拠したメール送信機能を簡単に取り込む事ができます。

ただ少々AWS上での設定作業は必要となります。もし貴方が自分自身のドメインネームを持っているのであれば、それを使ってメールを送信するために、DNSをいじる必要があります。(もし持っていない場合はメールアドレスさえ持っていればSESを使えます。この節は飛ばして次の節に進みましょう。)

私は『**abarth.me**』というドメインを所有しています。ここでは、このドメインを持つメールアドレス、例えば**no-replay@abarth.me**をメールの送信元(From)に設定したメールをLambdaから送信する事を目指します。皆さんは自分の持っているドメイン名に置き換えて設定を進めましょう。

では『IDの作成』をクリックします。**IDタイプ**は『**ドメイン**』を選び、**ドメイン**に『**abarth.me**』を記入し、ページ下部の『IDを作成』をクリックします。ドメインをAWSのサービスであるRoute53で管理している場合は、DNSの設定もやってくれますが、今回はドメインは別のDNSサービスで管理しているとします。

DKIMを有効にするためにはDNSにいくつかCNAMEを足してやる必要があります。ページの下の方にCNAMEが三つ表示されています。この名前と数値をDNSに設定してやりましょう。DNSは使っているサービス毎に設定方法が異なりますので、その変はご自身で調べてください。

さて、これの浸透にしばし時間がかかりますので、ここで、ごはんタイムにすると良いと思います。設定画面の『**IDステータス**』や『**DKIMの設定**』が『**保留中**』から『**検証済み**』『**成功**』変わったら準備完了です。

SESは最初はサンドボックスと呼ばれる検証環境で動いており、送信元だけでなく、送信先も、AWS上で設定したドメインやアドレスにしかメールを送る事ができません。そこで初めての送信先として、自分自身のメールアドレスを設定しましょう。

# SESメールアドレス認証

> この節で使うツール「[Amazon SES > 検証済みID](https://eu-central-1.console.aws.amazon.com/ses/home?region=eu-central-1#/verified-identities)」

ここではメールアドレスをSESに登録します。例えばSESでは、前節で指定したドメインに属するメールアドレス、あるいはここで設定する個別のメールアドレスしかFromに設定できません。またSESはデフォルトではサンドボックス環境で動作しており、動作テストなどはこの環境で行いまず。サンドボックス環境では送信先も、SESに登録したドメインもしくはアドレスでないといけませんですので、前節のドメインを登録している人は最低一つ、登録していない人はFrom用とTo用で二つ登録しましょう。(もちろんFromとToが同じアドレスでも良いとは思いますが、ちゃんと届いている感が若干低減してしまいますので、二つ登録しましょう)

では、メールアドレスを追加していきましょう。『**検証済みID**』のページで『**IDの作成**』ボタンを押下し、今度は『**Eメールアドレス**』を選択して、メールアドレスを入力し、ページ下部の『**IDの作成**』をクリックします。

すると、入力したメールアドレスへ確認メールが送信されます。このメール本文に書かれたURLをクリックすれば、検証が終了です。

やっと、これでLambda関数を実装する準備が整いました。

# Lambda関数実装

> この節で使うツール「[Lambda](https://eu-central-1.console.aws.amazon.com/lambda/home?region=eu-central-1#/discover)」

もし皆さんが古くはCGI、あるいはPHPでWebアプリケーションを実装した事があるのならば、Lambdaが何かを知る必要がありません。古くからあるプログラミング用語で『Lambda関数』というのがありますが、それのクラウド版が『AWS Lambda』なのですが、そもそもCGIやPHPの動作原理はLambdaと同等ですので、Webプログラマーな皆さんは、AWS Lambdaが何であるかを調べて頭でっかちになる必要はありません。

PHPで実装した『index.php』。これを特定のディレクトリに置けば、『 http://hogehoge/index.php 』にアクセスがあった時に実行されます。『index.php』の中では、echoを使って、HTMLのソースコードを出力します。これはアクセス元のユーザのブラウザでWebページとして表示されます。

これは、そのままLambdaの概念に合致しますし、今回Lambdaでやろうとしている事です。

ではまず『**関数の作成**』をクリックしましょう。次の画面で『**一から作成**』を選びます。関数名はなんでも良いのですが、ここでは『**testLambdaSES**』としましょう。ランタイムは『Node.JS 18.x』が執筆時点でのバージョンのようで、これを選びます。アーキテクチャはどっちでも良いです。重要なのはその下『**デフォルトの実行ロールの変更**』です。

『**デフォルトの実行ロールの変更**』が折りたたんであったら展開してください。ロールは先ほど作りましたので、**実行ロール**は『**既存のロールを使用する**』を選びます。**既存のロール**から、先ほど作ったロール(**TEST_SES_ROLE**)をチェックします。最後に『**関数の作成**』をクリックします。

これでLambda関数の枠組みができました。次にLambda関数そのものを実装します。今画面に**関数の概要**という画面がでていると思います。もし出ていなかったら、[こちら](https://eu-central-1.console.aws.amazon.com/lambda/home?region=eu-central-1#/functions)から、関数名をクリックしてください。

ページを下へスクロールすると**コードソース**が表れます。index.mjsのソースコードが表示されているはずです。

```javascript:index.mjs
export const handler = async (event) => {
  // TODO implement
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  return response;
};
```

**handler**という非同期関数を作成し、その返り値でステータスコードやレスポンスのbodyを渡すという作りだという事が分かります。つまりここをいじる事によって、任意の処理が行えそうです。

では、ソースコードを以下のコードに変更しちゃいましょう。コードの意味はあとで説明しますが、二か所だけ書き換えてください。

1. %% 送信先アドレスをここへ %%
1. %% Fromのアドレスをここへ %%

```javascript:index.mjs
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const TO_ADDR = '%% 送信先アドレスをここへ %%';
const FROM_ADDR = '%% Fromのアドレスをここへ %%';
const REGION = 'eu-central-1'; //リージョンが違う場合は変える
const MAIL_SUBJECT = 'First Test Mail from AWS Lambda';

const sesClient = new SESClient({ region: REGION });
export const handler = async (event) => {
    const getParam = (key) => {
        if (event.hasOwnProperty('queryStringParameters')) {
            //From Web
            return event.queryStringParameters.hasOwnProperty(key) ? event.queryStringParameters[key] : '未指定';
        } else {
            //From Test
            return event.hasOwnProperty(key) ? event[key] : '未指定';
        }
    };
    let key1 = getParam('key1');
    let key2 = getParam('key2');
    let key3 = getParam('key2');
    const BODY_HTML = `
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test Lambda SES</title>
  </head>
  
  <body>
  <h1>クエリストリング</h1>
    <ul>
        <ui><b>Key1</b>: ${key1}</ui>
        <ui><b>Key2</b>: ${key2}</ui>
        <ui><b>Key3</b>: ${key3}</ui>
    </ul>
  </body>
</html>`;

    const BODY_TEXT = `
Test Lambda SES
クエリストリング
Key1: ${key1}
Key2: ${key2}
Key3: ${key3}
    `;
    const response = {
        statusCode: 200,
        headers: {
            'Content-Type': 'text/html',
        },
        body: BODY_HTML,
    };

    const sendEmailCommand = new SendEmailCommand({
        Destination: {
            /* required */
            CcAddresses: [],
            ToAddresses: [TO_ADDR],
        },
        Message: {
            Body: {
                Html: {
                    Charset: 'UTF-8',
                    Data: BODY_HTML,
                },
                Text: {
                    Charset: 'UTF-8',
                    Data: BODY_TEXT,
                },
            },
            Subject: {
                Charset: 'UTF-8',
                Data: MAIL_SUBJECT,
            },
        },
        Source: FROM_ADDR,
        ReplyToAddresses: [],
    });
    try {
        await sesClient.send(sendEmailCommand);
        return response;
    } catch (e) {
        console.error('Failed to send email.');
        return e.toString();
    }
};
```

コピペしたら『**Deploy**』ボタンをクリックし、デプロイが成功したら『**Test**』をクリックします。テストはこのLambda関数にパラメータを渡しつつ起動して動作確認を行う機能です。

まず名前を付けなければなりませんので、これも適当な名前を付けます。私は『test-lambda-ses-event』としました。**test-lambda-ses-event**は『**プライベート』**、**テンプレート - オプション**は『**hello-world**』のままで大丈夫です。

**hello-world**のイベントJSONは以下のように設定されています。
```json
{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}
```
これはパラメータとしては三組の値を与えてこのLambda関数を実行する事を意味しています。Lambda関数のソースコードの中でこのkey1～3の値を取得する個所がありますので、確認してみましょう。value1～3は好きな文字列に変えてOKです。

『**保存**』をクリックすると、さっきの画面に戻りますので、もう一度『**Test**』をクリックすると、実行されます。

成功したら、**Execution Result**のタブにResponseが表示されるとともに、**TO_ADDR**に指定したメールアドレスにもメールが届いているはずです。Resopnseの文面、メールの文面ともに、key1～3で指定した値が表示されている事も確認してください。

# API Gateway

さて次に、外部からアクセスできるURLとこのLambda関数を接続します。[API Gateway](https://eu-central-1.console.aws.amazon.com/apigateway/home?region=eu-central-1)は独自のツールですが、今回のシナリオではLambdaの画面から設定するのが簡単です。

前節でテストをした画面の上の方に戻ると、『**トリガーを追加**』というボタンがあります。これをクリックしましょう。**ソースを選択**というドロップダウンメニューが出てきますので『**API Gateway**』を選びます。

**Intent**は『**Create a new API**』、**API Type**は『**REST API**』、**Security**は『**Open**』を選び、『**追加**』をクリックします。

出てきた画面を少し下にスクロールすると、トリガーにAPI Gatewayが定義されている事が分かります。どこだかわからない人は、もう一度**Lambdaの画面**から**関数名をクリック**し、コード表示の所の**タブを設定に変更する**と出てきます。それでも出てこなかったら**左側の垂直メニューからトリガーを選ぶ**とでてきます。

このAPI Gatewayの所の『**API endpoint:**』という欄に続いてURLが書いてあります。このURLが、今回定義したLambda関数を呼び出すためのURLです。クリックすると画面に結果が表示されるとともに、同じ内容のメールも送られます。

このURLはパラメータが何も指定されていませんので、何か指定してみましょう。例えばブラウザに表示されているURLの末尾に以下の文字列を追加してみてください。
```
?key1=hogehoge&key3=foobar
```

画面表示とメールともに、key1とkey3に指定された文字列が格納されている事が分かります。

さて、クエリストリング経由でパラメータを渡せるという事は、GETメソッドに対応したWebアプリケーションで、メールの送信ができたという事と同等です。すこしの改良でPOSTにも対応できます。

# まとめ

この文書では、AWSをゼロから始める状態で、これまでPHP等で実装されていた、ブラウザからのアクセスに動的にHTMLを返すという基本的な構成に加えて、メールを送るという付加機能をつけたサービスをAWS Lambdaで実現する最も簡単な方法を解説しました。

## ご注意

今回作成したものは、URLを叩けばメールが送信されるというツールです。一般的にはこれは非常に危険です。URLがもし外部流失し、だれかがいたずらで大量のアクセスを送ったら・・・、あなたのメールボックスはパンクするでしょう。実際の応用ではこのような作りになる事はありませんが、今回は説明をLambdaとSESの連携にフォーカスするために、このような作りになっています。**今回作ったもの、特にAPI Gatewayは必ず削除しましょう**。以下のツールを開いて、該当するエントリをチェックし『**削除**』ボタンを押します。

> この節で使うツール「[API Gateway](https://eu-central-1.console.aws.amazon.com/apigateway/main/apis?region=eu-central-1)」