AWS上の静的Webサイトの問合せフォームからメールを送信する(SES+Lambda+API Gateway+ちょっとRoute53)

どうも、Tです。

お問合せフォームはWebサイトで割と必要になる場面が多いですが、お問合せフォームのためだけにWordPress準備したり、PHPをインストールしたサーバーを用意するのも面倒なので、今はやりのサーバーレスに挑戦してみました。

先に言っておくとやっぱり知らずにAWSのサービスを使いこなすのは非常に面倒でした。慣れたら楽なんだろうけど・・・。バリデーションやエラー処理はほぼ行っていないのでご注意ください。

やりたいこと

概要

Webサイト(ここではCloudFront)に問い合わせページ(フォーム)を設置して入力した内容を自分(Webサイトの管理者宛て)にメール送信したい。

書き忘れましたが、CloudFrontのOriginにはS3を利用している前提で進めます。

問い合わせフォームと届くメール

名前・Eメールアドレス・会社名・問い合わせ内容など基本的な項目のフォームを準備して、登録ボタンを押すと・・・・

自分(Webサイトの管理者宛て)のメールアドレスに、お問合せ内容が届きます。件名には問合せ番号(問い合わせ時間をYYYYMMDDhhmmss形式)にします。

自分だけに届けばいいので、差出人と宛先は同一のメールアドレスで進めます。

環境

使う言語

HTML・JavaScript

お問合せページを作成するのに使います。とりあえず動作確認なので、CSSは使ってません。

Python

Lambdaで問合せデータの加工やメール送信処理に使います。Node.jsでも大丈夫です。Python勉強したかったので使ってみました。

使うサービス

Amazon SES(Simple Email Serivce )

Amazon SES(高可用性で低価格なEメール送信サービス)| AWS
Amazon SESは、デジタルマーケティング担当者やアプリケーション開発者がマーケティング、通知、トランザクションに関するEメールを送信できるように設計された、クラウドベースのEメール送信サービスです。Eメールを利用してお客様とのつながりを維持するあらゆる規模の企業を対象とした、コスト効率の高い信頼できるサービスです...

SESは、メール送受信が行えるサービスです。今回はこの送信機能(SMTPサーバーっぽい)だけを利用します。

料金はLmabdaから送信するので、「E メールクライアントやその他のソフトウェアパッケージからの E メール送信」に該当するので1000通につき0.10USDと格安です。1000件問合せ来られても困りますが・・・・。

ここの費用はおそらくです。Lambdaを利用した場合どの料金になるのかちゃんと調べてません。

AWS Lambda

イベント発生時(今回は問合せフォームの登録ボタンが押されたとき)に、処理を行ってくれるサービスです。フォームのデータを受け取って加工、メールを送信する処理を行わせます。PHPの代わりのようなものです。今回は、Pythonで記述します。

料金は面倒細かいので割愛しますが、処理時間に比例するので安いです(問い合わせがあったときの処理時間に従量課金)が、ループするような処理をしてしまうと爆上げ請求くるのでご注意ください。

Amazon API Gateway

APIを公開できるサービスです。処理はLambdaで行いますが、Webサイトのフォームから直接Lambdaにイベント通知は行えないので、問合せフォームとLambdaのつなぎ役になります。

イメージ的には、

問い合わせ者 → HTMLファイル → jsファイル → API Gateway → Lambda

になります。

前提

  • AWSマネジメントコンソールが利用できる
  • 必要なサービスにアクセスできるアカウントを持っている
  • CloudFrontでWebサイトの公開はできている
  • 実在するメールアドレスを持っている(差出人・宛先に利用)
  • 特筆しない限り、東京リージョンで操作しています

設定方法

Webページ準備

HTMLソース

問い合わせフォーム(contact.html)です。jQueryのAjaxを利用するためscriptでリンクしてます。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>問い合わせ</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="contact.js" defer></script>
</head>

<body>

    <main>
        <h2>問い合わせフォーム</h2>

        <!--START フォーム領域-->
        <form id="myform">
            <!-- 名前 -->
            <div>
                <label for="name">名前</label>
                <input type="text" id="name" name="name" placeholder="名前を入力してください" />
            </div>

            <!-- Eメールアドレス -->
            <div>
                <label for="email">E-Mail</label>
                <input type="email" id="email" name="email" placeholder="Eメールアドレスを入力してください" />
            </div>

            <!-- 会社名 -->
            <div>
                <label for="company">会社</label>
                <input type="text" id="company" name="company" placeholder="会社名を入力してください" />
            </div>

            <!-- お問合せ内容 -->
            <div>
                <label for="content">内容</label>
                <textarea id="content" name="content" placeholder="お問合せ内容を入力してください"></textarea>
            </div>

            <!-- 送信ボタン -->
            <input type="button" value="登録">
        </form>
        <!--END フォーム領域-->

        <!--メッセージ表示領域-->
        <p id="msg"></p>
    </main>
</body>

</html>

JavaScriptソース

フォームの内容を取得してAPI Gatewayにajax通信するjs(contact.js)です。apiurlにyは、API Gatewayを設定した後に生成される呼び出しURLを記載します。

function regist(){
    // 変数定義
    var apiurl = 'API Gatewayの呼び出しURLを記載する';
    // formタグの中身を取得
    var form = $('#myform');
    // formタグ内のvalue値を取得
    var formdata = form.serialize();

    // Ajax通信
    $.ajax({
        // Ajax定義
        type:'POST',
        url:apiurl,
        data:formdata,
        dataType:'json'
    }).done(function(data, textStatus, jqXHR){
         if(data.result == 1){
            // Lambdaからの返答が正常だった場合
            $('#msg').html('お問合せありがとうございました。');
        }else{
            // Lambdaからの返答がエラーだった場合
            console.log(data.result );
            $('#msg').html('エラーが発生しました。');
        }
    }).fail(function(jqXHR, textStatus, errorThrown){
        // Ajaxの通信に問題があった場合
        $('#msg').html('不明なエラーが発生しました。');
    })

}

AWS SES設定

SESでメールが送信できるようにしていきます。SESは実際に存在するメールアドレスを検証する必要があるため、実在するメールアドレスが必要です。

ドメインの検証

SESからメールを送信するためにドメイン名(メールアドレスの@より後ろ)の検証を行います。

SES画面でDomains→Verify a New Doaminをクリックします。

Domainにドメインを入力、Generate DKIM Settingsにチェックを入れ、Verify This Domainをクリックします。

今回は、自分自身にメールを送信するためDKIMはなくても大丈夫ですが、拡張の構想があったのでチェックを入れています。

内容を確認し、Use Route 53をクリックします。

今回、DNSにRoute 53を使用してるので、クリックで自動設定できます。ドメイン取得に他のDNSサービスを利用している場合は、表示されているレコードを手動で登録してください。

Create Record Setsをクリックします。

WARNINGで表示されていますが、レコードの置き換えが発生するので、すでMXレコードを登録している場合は、影響がないか事前に確認しましょう。

WARNING: This option will replace all existing MX records for your domain. Do NOT select this option unless you are specifically setting up your domain to receive email through Amazon SES, as described in Receiving Email with Amazon SES.

登録したドメインのステータスが下記になっていることを確認します。

Verification Status:verified

Enabled for Sending:Yes

これでドメインの検証が行えました。

メールアドレスの検証

次にメールアドレスの検証を行います。

Email Addresses画面でVeify a New Email Addressをクリックします。

検証したドメインで、差出人に利用するメールアドレスを入力しVerify This Email Addressをクリックします。

入力したメールアドレスに確認メールを送った旨の表示がされるのでCloseをクリックします。

下記のようなメールが届くので枠で囲んでいるURLをクリックします。(有効期間は24時間です)

クリックするとSESの紹介ページに飛ばされますが、こちらは使用しないので閉じておきます。

SESの画面で先ほど入力したメールアドレスのVerification Statusがverifiedになっていることを確認します。

これでメールアドレスの検証が完了しました。

メール送信テスト

SESでメールが送信できるかテストしておきます。メールアドレスにチェックをしてSend a Test Emailをクリックします。

To、Subject、Bodyを入力して、Send Test Emailをクリックします。FromとToは同じアドレスを指定してください。

SESでは、デフォルトで検証したメールアドレスにしか送信できない制限(sandbox状態)になっています。そのためにToにFromと同じアドレスを指定しました。外部のドメインのメールアドレスに送信する場合は、Request a Sending Limit Increaseからsandboxの解除申請をしてください。(他の方の記事などおおむね申請の承諾に1日以上かかっています。)

メールが届きました。

IAMロール準備

Lambda実行とSESを操作できるロールを作成します。

IAMサービスのロール画面からロールの作成をクリックします。

AWSサービス→Lambdaを選択し次のステップ:アクセス権限をクリックします。

下記のポリシーをチェックして次のステップ:タグをクリックします。

  • AWSLambdaBasicExecutionRolet
  • AmazonSESFullAccess

わかりやすいようにAmazonSESFullAccessを与えていますが、SESでメールを送信するには、下記の実行ロールだけでOKです。

Lambda と Amazon SES を使用して E メールを送信する
AWS Lambda と Amazon Simple Email Service (Amazon SES) を使用してメールを送信したいと考えています。

次のステップ:確認をクリックします。

ロール名の入力とポリシーを確認して、ロールの作成をクリックします。

ロールが作成できました。

Lambda関数準備

メール送信するLambda関数を作成していきます。

Lambda関数のコード

Lambdaで利用するPythonのコードです。変数「CLIENT」「MAIL_FROM 」「MAIL_TO」は各自の環境に置き換えてください。

import boto3        #AWSサービスを扱うためのライブラリ
import json         #JSONを扱うためのライブラリ
import urllib.parse #URL解析のためのライブラリ、POSTデータの抽出に利用
import time         #タイムスタンプ取得に利用
import datetime     #年月日取得に利用する


################################################################################
# 現在日時取得。
# LamdaはUTC時刻になるため、JSTに変換を行う。
################################################################################
def JstNow():
    #Unixタイムスタンプ取得(UTC時間)
    utc_unixtimestamp = int(time.time())
    #JST+9時間(32400秒)加算
    jst_now = datetime.datetime.fromtimestamp(utc_unixtimestamp+32400)
    
    return jst_now



################################################################################
# メール送信処理。
# データ成型とメール文を作成して送信。
################################################################################
def SendMail(event,context):
   
    # 現在日時取得
    NOW_TIME = JstNow()



    # メール送信情報定義
    CLIENT = boto3.client('ses', region_name="SESのリージョン")               #SESの利用とSESリージョンの指定
    CHARSET = 'UTF-8'                                                   #文字コード指定
    MAIL_FROM = 'SESで検証したメールアドレス'                              #メール差出人アドレス
    MAIL_TO =  'SESで検証したメールアドレス'                               #メール宛先アドレス
    MAIL_SUBJECT = 'お問合せNO:' + NOW_TIME.strftime("%Y%m%d%H%M%S")   #メール件名

    
    
    # メール送信内容作成
    # URL内のPOST内容パース。POSTの内容は、URLのbodyの中に入るため、bodyをパース。
    PARAM_BODY = urllib.parse.parse_qs(event['body'])
    
    CONTACT_DATETIME = NOW_TIME.strftime("%Y年%m月%d日 %H:%M")    #問合せ日時形式を変換
    CONTACT_ADDRESS = PARAM_BODY['email'][0]                        #お問合せ者メールアドレス
    CONTACT_NAME = PARAM_BODY['name'][0]                            #お問合せ者氏名
    CONTACT_COMPNAY = PARAM_BODY['company'][0]                      #お問合せ者会社名
    CONTACT_CONTENT = PARAM_BODY['content'][0]                      #お問合せ内容
 
    CONTACT_BODY ="【お問合せ日時】\r\n"\
                    +CONTACT_DATETIME+"\r\n\r\n"\
                    +"【メールアドレス】\r\n"\
                    +CONTACT_ADDRESS+"\r\n\r\n"\
                    +"【問合せ者名】"+"\r\n"\
                    +CONTACT_NAME+"\r\n\r\n"\
                    +"【会社名】"+"\r\n"\
                    +CONTACT_COMPNAY+"\r\n\r\n"\
                    +"【内容】"+"\r\n"\
                    +CONTACT_CONTENT+"\r\n\r\n"
                  
              
    try:
        #メール送信
        RESPONSE = CLIENT.send_email(
            Source=MAIL_FROM,
            Destination={
                'ToAddresses': [
                    MAIL_TO,
                ]
            },
            Message={
                'Subject': {
                    'Data':  MAIL_SUBJECT,
                },
                'Body': {
                    'Text': {
                        'Charset':CHARSET,
                        'Data': CONTACT_BODY,
                    },
                }
            }
        )
    
    except:
        # メール送信が失敗した場合
        body = {'result':0}
        return{
            'statusCode' : 200,
            'headers': {
                'Access-Control-Allow-Origin' : '*', # CORSの対策
                'content-type':'application/json'
            },
            'body': json.dumps(body)
            
        }
        
    else:
        # メール送信が成功した場合
        body = {'result':1}
        return{
            'statusCode' : 200,
            'headers': {
                'Access-Control-Allow-Origin' : '*', # CORSの対策
                'content-type':'application/json'
            },
            'body': json.dumps(body)
            
        }

関数の作成

Lambdaサービスの関数画面から関数の作成をクリックします。

下記を設定し関数の作成をクリックします。

  • 関数の作成:一から作成
  • 関数名:Test_Func_SendMail
  • ランタイム:Python 3.7
  • 実行ロール:既存のロールを使用する
  • 既存のロール:TestRole_SESbyLambda ※先ほど作成したロール

lambda_function.pyの中に、先ほど準備したコードを張り付けてsaveをクリックします。

基本設定の編集をクリックします。

ハンドラをlambda_function.SendMailに入力して保存をクリックします。

SESでメールを送るLambda関数ができました。

API Gateway設定

API Gatewayで先ほどのLambda関数を紐づけていきます。

APIの新規作成

API GatewayサービスのAPI画面のAPIを作成をクリックします。

REST APIの構築をクリックします。

下記を設定してAPIの作成をクリックします。

  • プロトコルを選択する:REST
  • 新しいAPIの作成:新しいAPI
  • API名:TestAPI_SendMail
  • エンドポイントタイプ:リージョン

リソースの作成

アクションからリソースの作成をクリックします。

リソース名にsendmailを入力してリソースの作成をクリックします。

メソッドの作成

先ほど選択したリソースを選択した状態でアクションのメソッドの作成をクリックします。

メソッドのリストからPOSTを選択します。

チェックボタンをクリックします。

POSTのセットアップ画面で下記を設定して保存をクリックします。

  • 統合タイプ:Lambda関数
  • Lambdaプロキシの統合の使用:チェックあり
  • Lambdaリージョン:ap-northeast-1(東京リージョン)
  • Lambda関数:Test_Func_SendMail(先ほど作成したLambda関数名)

権限追加の確認画面でOKをクリックします。

メソッドが作成できました。

APIのデプロイ

APIのメソッドは作っただけでは外部から使える状態ではありません。外部からAPIを叩けるように、APIのデプロイを行います。

アクションからAPIのデプロイをクリックします。

初めてのデプロイでは、下記の画面が表示されます。設定を行いデプロイをクリックします。

  • デプロイされるステージ:新しいステージ
  • ステージ名:Prod(任意の名前です)

デプロイされました。ステージ画面のPOSTをクリックしてURLの呼び出しのURLをコピペします。

S3へコンテンツのアップロード

コピペしたAPI GatewayのURLをcontact.jsのapiurlに設定します。

contact.htmlとcontact.jsをS3にアップロードします。

動作確認

Webブラウザでcontact.htmlにアクセスします。名前、E-Mail,会社、内容を入力して登録をクリックします。

バリデーションしていないのですべて入力しないとLambda側でエラーになるのですべて入力してください。

正常に動作すると画面下部に「お問合せありがとうございました。」が表示されます。

メールクライアントを見ると受信できていることが確認できます。

ちょっといけてないところ

問い合わせ者にはメールが届かない

問い合わせ者にもメールが届くのがいいんでしょうが、外部のメールアドレスに送付するとなると、送信用のメールアドレスを取得する必要がある、SESのsandbox解除しないといけないので自分が分かれば、あとは通常のメールやり取りでいいかと割り切りました。

問い合わせ番号が時間

識別のために問合せ番号を生成したかったので、現在日時を秒まで取得して問合せ番号にしています。秒単位でのバッティングはまずありえない規模なので問題はないのですが、秒単位で被るとまずい場合は、Lambda単体では番号を保持できないので、RDBに保存するかS3に連番用のファイルを用意するなどの対処になりそうです。

どこかで失敗すると問合せができない

API GatewayかLambdaかjsの処理か・・・どこかで失敗すると問合せできません。対応方法として、失敗のステータスになった場合は、問合せ先のページを表示させるなどで対応を考えています。これは、WordPressとかでも起こりうる問題ですね。

躓きポイント

API Gatewayをどう呼ぶか?

API Gateway自体は、HTMLのformタグやJavaScriptのfetchなどでも叩けるんですが、最終的にAjaxを使うことにしました。

HTMLのformタグ→エラーが出ても対処できねぇ・・・

fetch→IE対応してねぇ

なんですが、開発経験が少ないので、遠回りましました・・・。

API GatewayのLambdaプロキシ統合の使用にはまる

下記のLambdaプロキシ統合の使用は、API Gatewayがリリースされて途中で登場した機能です。

ブログでもプロキシ統合ありなしで、混在して書かれていて設定方法が異なるのでドンハマりしました。ありなしの違いについては、下記がよくまとまっておりました。

[初心者向け] Lambda 非プロキシ統合で API Gateway API をビルドする をプロキシ統合にして比較してみる | DevelopersIO

Lambdaプロキシ統合でCORS対応問題

CORSの問題で動きませんでした・・・。CloudFront使ってる部分の問題なんですが・・・。

API Gatewayのリソース作成の時にCORSの有効化にチェック欄があり作成した後も手動で有効化することができます。

しかし、プロキシ統合の場合Lambda側でレスポスヘッダを明示しないといけないことまで気づくのに時間がかかりました。

AWS APIGateway + Lambda プロキシ統合 の利用でCORS設定にはまった話 - Qiita
#要約AWSのAPIGatewayを利用してAPIを作ったが、CORSの設定がどうも上手くいかない、、、という所にはまった話です。(昔作ったAPIは動くのに、、、なんで!?( ゚д゚)ハッ! )…

公式もドキュメントあるんですが、イマイチわかりにくい・・・。

API Gateway での REST API の CORS - Amazon API Gateway
クロスオリジンリソース共有 (CORS) とは何か、有効にするかどうか、および API Gateway で CORS メソッドを有効にする方法について説明します。

Lambdaプロキシ統合のレスポンス形式がよくわからない

公式マニュアルもあるんですが、returnの返し方がいまいちわからずハマりました。下記に詳しく記載いただいてます。

LambdaのタイムゾーンがUTC問題

問い合わせ時間をメール本文に記載するために、現在時刻を取得しているんですがLambdaのタイムゾーンはUTC時刻しか取れません・・・・仕様です・・・・。

なので、Lambda側ので datetime.datetime.fromtimestamp(utc_unixtimestamp+32400)によって32400(9時間)足して変換してます。

Lambdaの環境変数TZを変更すればいいというブログがあったので、それでやってたんですが、非推奨のようです。実際マニュアルにもTZは予約済み環境変数で記載されていました。

Lambda 環境変数を使用したコードの値の設定 - AWS Lambda
Lambda で環境変数を使用する方法を説明します。 環境変数を使用して、コードを更新することなく関数を調整します。

下記で詳しく解説されております。

参考

全体

AWS APIGateway/LambdaとJavascriptで簡易問い合わせサイトをつくる - Qiita
#概要問い合わせフォーム(javascript) ⇒ APIGateway ⇒ Lambda(Node.js) ⇒ Lambda(Node.js) の流れで簡単な問い合わせサイトを作ります。一応…
Lambda と Amazon SES を使用して E メールを送信する
AWS Lambda と Amazon Simple Email Service (Amazon SES) を使用してメールを送信したいと考えています。

SES

Amazon SESによるメール送信環境の構築と実践 | DevelopersIO

CORS

https://note.com/kanoemon145/n/n48a50b62dd85

まとめ

最初は簡単かとなめてかかったんですが、メール送るだけでこんな面倒なん?というのが正直な感想です・・・。サーバーレスで何かしようとすると、このあたりの理解は必須になるので良い勉強になったかな・・・初めてPython使ったのでもっときれいに書けるようになりたい・・・・。