Amazon SESのメール送信をLambda Powertools PythonとAPI Gatewayで実現

Amazon SESのメール送信方法を紹介します。

Ishiguro Suguru

Webサイトの登録依頼画面でユーザーが送信ボタンを押すとメールをAmazon SESで送信する設定を行ったのでそのメモです。

AWS SES、Lambda等々でググると参考記事がたくさん出てきますが、 今回はAWS Lambda Powertools Pythonで実装して少しハマったので記事にしました。

ちなみにWebの登録画面は以下です。

https://antena.itsys-tech.com/register

参考サイト

→第1回はライブラリの導入方法、第5回はAPIの利用方法が記載されている。
その他の回で紹介されているが、Lambda PowertoolsはAPIの他に便利な機能(Tracer, Logger, Metrics)を使えるようなのでどこかで紹介したい。

→Amazon SESの設定方法。管理コンソールから行うがAWSのWebサイトは頻繁に更新されるので参考程度に。

対応概要

ゴール

  • 以下の流れで送信先に設定したアドレスにメールを送信する。

Webサイト登録依頼画面で送信ボタンクリック → API Gateway → Lambda → Amazon SES → メール送信
※フロントエンド側の実装については割愛

作業概要

  1. Amazon SESの利用設定
    →メールの送信先など設定を行う
  2. pythonコード実装
  3. AWS SAMでLambda等の必要なリソース作成

1. Amazon SESの利用設定

ここでやることは以下3つの設定。

  • 本稼働アクセスのリクエスト(任意)
  • IDの作成(ドメイン)
  • IDの作成(Eメールアドレス)

この辺の設定はやりたいことに応じて設定するのが良いです。

詳しいことは以下のサイトで解説されてますが、 セキュリティ等の理由で、事前にIDの作成で登録した送信先にのみメールの送信は可能です。 登録したメール以外にも送りたい場合は「本稼働アクセスのリクエスト」を行う必要があります。 今回は必要なかったのですが今後利用する可能性があるため申請しました。

以下、申請・ID登録の流れの画面ショット。
※一度却下されたがサポートページで詳細を返信して通った

本稼働アクセスのリクエスト

・Amazon SESのページを開きアカウントダッシュボードの本稼働へのアクセスをクリックする

・必要事項を入力してリクエスト

・却下メールが届くorz(申請して1~2時間後)

・サポートページで詳細を記入し返信

・無事承認される(半日以上は待った)

IDの作成

【前提】
・ドメインは本サイトのドメイン利用
・Route53でドメイン登録済み→別のドメインサービスを利用している場合はそっちで設定が必要

・Amazon SESの管理画面 > 検証済みID > IDの作成

・ドメインを選択してドメイン情報を入力

・作成したドメインのID情報が表示される。
AWSのRoute53でドメイン取得している場合は自動的に画面下のDNSレコードが登録されている。
レコードを登録するとステータスが検証保留→検証済みになる。
※ドメインサービスをAWS以外で使用している場合はそっちで登録必須

・しばらくするとステータスが検証済みになる

・次にEメールアドレスのID作成。
認証メールが指定したアドレスに届くのでメール内のリンクをクリックして認証完了させる。




2. pythonコード実装

  • AWS SAMでリソースは作成。
  • LambdaはDocker利用、実行コードはpython。

Lambda

srcディレクトリ配下に以下のファイルを置く。
※AWS SAMテンプレートからディレクトリ名を変更して作成

  • app.py: メイン処理実行
  • ses.py: メール送信処理
  • Dockerfile: Docker定義
  • requirements.txt: 使用ライブラリ記述

app.py

以下のコードのようにLambda Powertoolsを使うとルーティングが楽で見やすくなります。
POSTで送るデータのbodyもapp.current_event.json_bodyで簡単に参照できます。
テスト環境からこのAPIを実行する場合はCORS設定を行う必要があるので注意。

from aws_lambda_powertools.event_handler import APIGatewayRestResolver, content_types
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from aws_lambda_powertools.event_handler.api_gateway import CORSConfig, Response
from ses import send_mail

# [INFO] テスト時など必要に応じてコメントアウトを外してCORS設定を行う
# cors_config = CORSConfig(allow_origin="*", max_age=300)
# app = APIGatewayRestResolver(cors=cors_config)

app = APIGatewayRestResolver()


def lambda_handler(event, context):
    """Lambda function"""
    return app.resolve(event, context)


@app.post('/api/send/register')
def send_mail_register():

    FROM = "環境に合わせて変更 例)xxxx@xxxx"
    TO = "環境に合わせて変更 例)yyyy@yyyy"
    SUBJECT = "環境に合わせて変更"

    try:
        body_dict = app.current_event.json_body
        body_text = "\n".join(["{0}: {1}".format(key, value) for (key, value) in body_dict.items()])
        return send_mail(FROM, TO, SUBJECT, body_text)

    except TypeError as e:
        return {
            'isBase64Encoded': False,
            'statusCode': 500,
            'headers': {},
            'body': '{"message": "' + str(e) + '"}'
        }

@app.not_found()
def handle_not_found_errors(ex: NotFoundError) -> Response:
    return Response(
        status_code=501,
        content_type=content_types.TEXT_PLAIN,
        body="Not Implemented Error. Rooting is not found.",
    )

ses.py

import boto3
from botocore.exceptions import ClientError


def send_mail(from_address, to_address, subject, body):
    """ Amazon SES send mail

    :param from_address:
    :param to_address:
    :param subject:
    :param body:
    :return: send mail result
    """

    CHARSET = "UTF-8"
    client = boto3.client('ses')

    # Try to send the email.
    try:
        # Provide the contents of the email.
        response = client.send_email(
            Destination={
                'ToAddresses': [
                    to_address,
                ],
            },
            Message={
                'Body': {
                    'Text': {
                        'Charset': CHARSET,
                        'Data': body,
                    },
                },
                'Subject': {
                    'Charset': CHARSET,
                    'Data': subject,
                },
            },
            Source=from_address,
        )

    except ClientError as e:
        return {
            'isBase64Encoded': False,
            'statusCode': 500,
            'headers': {},
            'body': '{"message": "' + e.response['Error']['Message'] + '"}',
        }
    else:
        return {
            'isBase64Encoded': False,
            'statusCode': 200,
            'headers': {},
            'body': '{"message": "Email sent! Message ID:' + response['MessageId'] + '"}',
        }

requirements.txt

boto3
aws-lambda-powertools

3. SAM template.yaml

以下のコードでリソースを作成。 PyCharmでデプロイ。

Lambda Powertoolsの場合はAPI Gatewayはプロキシ統合にする必要があるらしい。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  python3.9

  SAM Template for sendMailForAntena

Globals:
  Function:
    Timeout: 60
    Environment:
      Variables:
        TZ: Asia/Tokyo
  Api:
    OpenApiVersion: 3.0.2
#    [info] CORS設定が必要な場合は以下コメントアウトを外す
#    Cors:
#      AllowOrigin: "'*'"
#      AllowMethods: "'GET,POST,DELETE,OPTIONS'"
#      AllowHeaders: "'X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept'"

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: SendMailRoleForAntena
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/"
#      [info] SESのポリシー(AmazonSESFullAccess)を付与する
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonSESFullAccess

  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      Name: SendMailApiForAntena
      StageName: prd

  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: SendMailFunctionForAntena
      Role: !GetAtt LambdaRole.Arn
      Timeout: 60
      MemorySize: 512
      PackageType: Image
      Architectures:
        - x86_64
      Events:
        SendMailApi:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGateway
            # [info] Lambda Powertoolsの場合はプロキシ統合形式になるため注意
            Path: /api/{proxy+}
            Method: ANY
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src
      DockerTag: python3.9-v1

  FunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref LambdaFunction
      Principal: apigateway.amazonaws.com

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LambdaFunction}
      # [info] ログの保持期間
      RetentionInDays: 7

Outputs:
  ApiGateway:
    Description: "API Gateway endpoint URL for Prod stage for SendMailFunctionForAntena"
    Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prd/api/send/"
  LambdaFunction:
    Description: "Lambda Function ARN"
    Value: !GetAtt LambdaFunction.Arn
  LambdaRole:
    Description: "Implicit IAM Role created for function"
    Value: !GetAtt LambdaRole.Arn

最後に

Lambda Powertoolsすごく便利そう。

comments powered by Disqus