poriweb

備忘録とか

AWS Lambda(python) / API Gateway / DynamoDB / SES でWebサイトのお問い合わせAPIを作る(DB保存,メール,slack連携)

サーバーレスでwebサイトを構築をすることがあったので、Lambdaネタです。
ググればすぐ出てくるような話が主ですが、ユースケースが合う人の参考になれば。
作成部分は簡単にしか書いていませんので詳しくはドキュメントを参照願います。
また、pythonで書いているので他言語は読み替えていただければと思います。

LambdaとAPI Gatewayを作成する

  • Lambda関数を作成する
    AWS WebコンソールよりLambda関数を作成します。
    ロールに権限が必要になるのでDynamoDBとSESにアクセスできるようにしておきます。

  • API Gatewayを作成する
    コンソールよりAPI Gatewayに行き新しいAPIの作成を押下し、API名とエンドポイントタイプ(リージョン)を入力/選択して作成します。
    空のAPIにリソースを設定します。アクションからメソッドの作成を選択し、POSTを作ります。Lambda関数の欄に先ほど作成したLambda関数を入力して保存。

f:id:t-pori418:20181213184253p:plain
API Gateway

また、CORSは有効化しておきます。POSTメソッドを選択し、アクションからCORSの有効化を行います。

f:id:t-pori418:20181213184510p:plain
API Gateway

あとはAPIをデプロイすればAPI Gateway側の基本設定は終了で、APIを実行できるようになります。

  • 最終的にLambda側のDesinerはこんな感じ(CloudWatchとS3にも権限を与えています)

f:id:t-pori418:20181213184527p:plain
Lambda

前提:お問い合わせのPOST内容

下記のようなjsonが送られてくるとします。関数のテストイベントの中身をこれにしておきます。

{
    "item": {
        "company_name": "テスト株式会社",
        "name": "テスト名前",
        "tel": "000-0000-0000",
        "email": "hogehoge@test",
        "content": "内容"
    }
}

1. Lambdaで入力されたものをDynamoDBに保存する

まず、DaynamoDBに contacts というテーブルを作成しておきます。

lambdaのコードは元々はこんな感じ

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

event で入力を受け取ります。APIの場合POSTされてきた中身がここに入ってきます。
POSTされてきたデータをlambdaに登録する処理を入れたコードはこんな感じ。

import json
from datetime import datetime
import boto3
from boto3.dynamodb.conditions import Key, Attr

dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    item = event['item']

    # 対象テーブル
    table = dynamodb.Table('contacts')

    # 実行時間を登録しておく
    item['received_at'] = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

    # テーブル登録
    table.put_item(
        Item=item
    )

    # 成功
    return {
        'statusCode': 200,
        'body': json.dumps('success!')
    }

登録された時間をテーブル内のデータとして持っておきたかったので datetime.now を使用しています。注意点としては日本時間にする場合はLambda関数の環境変数タイムゾーンを設定しておく必要がある点です。

f:id:t-pori418:20181213184552p:plain
Lambda

2. Lambdaでお問い合わせ内容をメールで管理者に知らせる

先にSESでドメインから送信できるように設定しておく必要があります。
仮に test.com というドメインを持っていて送信できる状態とします。

メール送信用に以下のような関数を用意します。

import boto3

MAIL_REGION = "us-east-1"

def send_email(source, to, subject, body):
    client = boto3.client('ses', region_name=MAIL_REGION)
 
    response = client.send_email(
        Source=source,
        Destination={
            'ToAddresses': [
                to,
            ]
        },
        Message={
            'Subject': {
                'Data': subject,
            },
            'Body': {
                'Text': {
                    'Data': body,
                }
            }
        }
    )
     
    return response

呼び出しは send_email("no-reply@test.com", "admin@test.com", "タイトル", ”本文”) といった感じです。
これを踏まえて先ほどのDB登録を行うコードに付け加えてみます。

import json
from datetime import datetime
import boto3
from boto3.dynamodb.conditions import Key, Attr

dynamodb = boto3.resource('dynamodb')

MAIL_REGION = "us-east-1"

def send_email(source, to, subject, body):
    client = boto3.client('ses', region_name=MAIL_REGION)
 
    response = client.send_email(
        Source=source,
        Destination={
            'ToAddresses': [
                to,
            ]
        },
        Message={
            'Subject': {
                'Data': subject,
            },
            'Body': {
                'Text': {
                    'Data': body,
                }
            }
        }
    )

    return response

def lambda_handler(event, context):
    item = event['item']

    # 対象テーブル
    table = dynamodb.Table('contacts')

    # 実行時間を登録しておく
    item['received_at'] = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

    # テーブル登録
    table.put_item(
        Item=item
    )

    # メール送信
    title = "お問い合わせが来ました"
    message = "会社: " + item['company_name']\
                + "\n名前: " + item['name']\
                + "\nメールアドレス: " + item['email']\
                + "\n電話番号: " + item['tel']\
                + "\n内容:" + item['content']\

    src = "no-reply@test.com"
    dst = "admin@test.com"
    rsponse = send_email(src, dst, title, message)

    # 成功
    return {
        'statusCode': 200,
        'body': json.dumps('success!')
    }

3. Lambdaでお問い合わせ内容をslackにも連携する

slack連携はwebhookを使用してPOSTすれば良いので urllib.request でなげちゃいます。これだけのためにRequestsを入れるのは面倒そうだったのでurllibを使用しています。

先ほどのコードにさらに追記しちゃいます。

import json
from datetime import datetime
import boto3
from boto3.dynamodb.conditions import Key, Attr
import urllib.request

dynamodb = boto3.resource('dynamodb')

MAIL_REGION = "us-east-1"

def send_email(source, to, subject, body):
    client = boto3.client('ses', region_name=MAIL_REGION)
 
    response = client.send_email(
        Source=source,
        Destination={
            'ToAddresses': [
                to,
            ]
        },
        Message={
            'Subject': {
                'Data': subject,
            },
            'Body': {
                'Text': {
                    'Data': body,
                }
            }
        }
    )

    return response

def lambda_handler(event, context):
    item = event['item']

    # 対象テーブル
    table = dynamodb.Table('contacts')

    # 実行時間を登録しておく
    item['received_at'] = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

    # テーブル登録
    table.put_item(
        Item=item
    )

    # メール送信
    title = "お問い合わせが来ました"
    message = "会社: " + item['company_name']\
                + "\n名前: " + item['name']\
                + "\nメールアドレス: " + item['email']\
                + "\n電話番号: " + item['tel']\
                + "\n内容:" + item['content']\

    src = "no-reply@test.com"
    dst = "admin@test.com"
    rsponse = send_email(src, dst, title, message)

    # slack連携
    url = 'https://hooks.slack.com/services/ slackから発行されたwebhooks url' # 自分のワークスペース,チャンネルのものに置き換えてください

    data = {
        'text': '<!here>\nお問い合わせが来ました。\n *会社* \n' + item['company_name'] + '\n *名前* \n' + item['name'] + '\n *メールアドレス* \n' + item['email'] + '\n *電話番号* \n' + item['tel'] + '\n *内容* \n' + item['content']
    }
    headers = {
        'Content-Type': 'application/json'
    }
    req = urllib.request.Request(url, json.dumps(data).encode(), headers)
    with urllib.request.urlopen(req) as res:
        body = res.read()

    # 成功
    return {
        'statusCode': 200,
        'body': json.dumps('success!')
    }

これで以下のようなメッセージが飛びます

@here
お問い合わせが来ました。
*会社*
テスト株式会社
*名前*
テスト名前
*メールアドレス*
hogehoge@test
*電話番号*
000-0000-0000
*内容*
内容

最後に

例として挙げたソースコードは各機能をくっつけただけかつエラーハンドリングまともにしてないなど修正、リファクタリングする必要はありますが、ただ実装するだけなら簡単にできました。
メールの送信実行以外は lambda_handler にほぼ処理を書いていてごちゃごちゃしてしまったのでこれから機能単位で分離していこうと思います。

また、実行しながら上記コードを書いているわけではないのでおかしいところなどありましたらご連絡していただけると助かります。