LambdaからSESを呼んでメール送信をサクッと実装してみた

2017.07.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、臼田です。

みなさんLambdaしてますか?

今回は、簡単なスクリプトを使っていて何かあったときに、とりあえずメールで通知を送る仕組みをLambdaとAmazon SESでサクッと作ってみたいと思います。

動機

現在Step FunctionsとLambdaでURLの更新チェックを行う仕組みを作っていますが、とりあえず作ったスクリプトが予期せぬエラーなどで動かなくなってしまったら困ります。

何よりも、エラーが起きたときに検知だけでもしたいので、エラーが発生したら自分のメールアドレス宛に通知がほしいなと思ってSESでサクッとできないか試してみました。

Step Functionsで作っている過程は下記をご参照下さい。

Step FunctionsにLambdaからデータを渡してみた

構成

今回はStep FunctionsがCatchしたエラーの内容をそのままLambdaからSDKを叩いてSESで送信する部分を作ります。

001_arch

SESの設定

SESはきちんとシステムとして利用しようと思ったら小難しい所があるかもしれませんが、試験用に利用する場合はサクッといけます。

流れとしては以下のような感じです。

  1. メールアドレス登録
  2. 登録メールアドレス確認
  3. テストメール送信

なお、本番環境で不特定多数へメールを送る場合には送信制限の解除申請が必要です。

詳細は下記をご参照下さい。

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

メールアドレス登録

SESの画面から「Email Address > Verify a New Email Address」を選択します。

001_ses

通知したいメールアドレスを入力して「Verify This Email Address」でメールアドレスを登録します。

002_mail

この段階では登録されただけで、ステータスが「pending verification」となって確認待ちのステータスです。

003_pending

登録メールアドレス確認

登録したメールアドレス宛に確認用のメールが届きますので、リンクを24時間以内に押します。

004_verifi

ステータスが「verified」となり確認完了です。

005_verified

テストメール送信

実際にメールを送ってみます。

登録したメールアドレスにチェックを入れて「Send a Test Email」を押します。

006_test

To:には登録したメールアドレス(From:と同じ)を、その他は適当に入れて「Send Test Email」を押します。

007_send

メールの受信が確認できます。

008_receive

LambdaからSESでメールを送る

コードは下記のとおりです。

今回もPython 3.6で作成します。

import boto3
import json

SRC_MAIL = "test@example.com"
DST_MAIL = "test@example.com"
REGION = "us-west-2"

def send_email(source, to, subject, body):
    client = boto3.client('ses', region_name=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):
    email = "error occured"
    message = json.dumps(event, indent = 4)
    r = send_email(SRC_MAIL, DST_MAIL, email, message)
    return r

メールアドレスやリージョンなどは本当は環境変数やS3等へ外出しするのが適切ですが、とりあえずやるならまあいいと思います。

それではLambdaを作成します。

コードがあるのでBlank Functionから作成します。

名前は適当に設定します。今回は「Sender」にします。

009_sender

上記のコードを直接入力します。

ロールはLambdaからSESへアクセスしてメールを送信するものを作成するために「カスタムロールの作成」を選択します。

010_role

IAM Roleの作成画面が開きます。

ロール名を適当に入れ、「ポリシードキュメントを表示」を展開してから「編集」を押します。

011_role_edit

確認画面が出るので「OK」を押します。

012_ok

ポリシーを編集できるようになるので、下記を入力します。(既存のStateの後ろにカンマを忘れずに)

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

内容としては、最初から作成してあるLambda用のStatmentに加えて、sesのメール送信のみ許可する設定を追記しています。

編集したら「許可」を押します。

013_role_create

Lambdaの画面に戻り、「既存のロール」が作成したものになっていることを確認して「次へ」を押します。

014_next

確認画面になるので「関数の作成」から作成します。

作成が完了したら、「テスト」からいつもの「Hello World」を実行します。

成功したら、Hello Worldがメールで届きます。

015_sendmail

これでLambdaの実装は完了しました。

StepFunctionsからエラーが発生したらLambdaを呼び出してSESでメールしてみる

され、それでは一連の流れにこのLambdaを組み込みます。

前回作成したStepFunctionsを複製します。(残念ながら、作成したものを編集できないので複製になります)

名前は適当に設定し、コードは下記のように変更します。

{
  "Comment": "This is CVE Update Checker",
  "StartAt": "UpdateChecker",
  "States": {
    "UpdateChecker": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-west-2:000000000000:function:Update_Checker",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "Next": "Sender"
        }
      ],
      "End": true
    },
    "Sender": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-west-2:000000000000:function:Sender",
      "End": true
    }
  }
}

変更しているところを説明します。

まず、前回はテンプレートのHello World!のStateをそのまま活用していましたが、適切な名前に変更しています。

また、Retryの要素の代わりに「Catch」を設定しています。

Catchはエラーの内容を定義し、そのエラーと一致した場合の次のStateを指定できます。

"States.ALL"で全てのエラーを対象にしていますが、エラーの種類ごと複数定義して処理を分けることも可能です。

「UpdateChecker」内でCatchで、次の処理に「Sender」を記述する事により、エラーが発生した場合の次のフローを決めています。

また、新しいStateとして「Sender」を増やしています。書き方は「UpdateChecker」とほぼ同じで、TypeはLambdaを実行するので「Task」として、この後の処理は無いので「"End": true」を含んでいます。

入力したコードが正しいかは、「ビジュアルワークフロー」の更新を押すと大体わかります。

綺麗になっていれば少なくとも流れは大丈夫でしょう。

「ステートマシンの作成」から作成します。

017_state

ロールは前回作成したものをそのままで「OK」を押します。

それではテストしてみます。

現状では前回と変わらずに、「Sender」を経由しないで終了します。

018_true

エラーを発生させる

今回は無理やりUpdate CheckerのLambda上でエラーを発生させます。

Update CheckerのLambdaを下記のように修正します。

import urllib.request
from datetime import datetime, timedelta

def lambda_handler(event, context):
	url = "https://s3-us-west-2.amazonaws.com/rules-engine/CVEList.txt"
	yesterday = datetime.now() - timedelta(1)
	try:
		with urllib.request.urlopen(url) as r:
			lm_header = r.info()['Last-Modified']
			last_modified = datetime.strptime(lm_header, '%a, %d %b %Y %H:%M:%S %Z')
			print(last_modified)
		if last_modified > yesterday:
			result = {"update": True}
		else:
			result = {"update": False}
		a = 1/0
	except Exception as e:
		raise e
	return result

エラーが発生する可能性がある外部リクエスト部分をtry-exceptでくくり、そのままraiseしています。

ついでに、エラーが確実に発生するように0で除算しています。

この状態でもう一度Stateを実行してみます。

「Sender」を経由して終了していることがわかります。

エラーは発生していますが、適切にraiseしているので、「UpdateChecker」では「成功」のステータスとなっています。

019_deny

メールがちゃんと届くことも確認できます。

020_errormail

以上で、エラーの発生をメールで確認することが出来るようになりました。

確認後は、Update CheckerのLambdaはもとに戻します。

さいごに

Step Functionsを利用して、エラー発生時の処理を別のLambdaで定義して、簡単に繋げることが出来ました。

もちろん、エラー毎に違うLambdaを実行したり、簡単に入れ替えたり出来ます。(Step Functions自体は再度作成する必要がありますが…)

次回はアップデートをTwitterに流してみます。