LambdaからSESを呼んでメール送信をサクッと実装してみた
こんにちは、臼田です。
みなさんLambdaしてますか?
今回は、簡単なスクリプトを使っていて何かあったときに、とりあえずメールで通知を送る仕組みをLambdaとAmazon SESでサクッと作ってみたいと思います。
動機
現在Step FunctionsとLambdaでURLの更新チェックを行う仕組みを作っていますが、とりあえず作ったスクリプトが予期せぬエラーなどで動かなくなってしまったら困ります。
何よりも、エラーが起きたときに検知だけでもしたいので、エラーが発生したら自分のメールアドレス宛に通知がほしいなと思ってSESでサクッとできないか試してみました。
Step Functionsで作っている過程は下記をご参照下さい。
構成
今回はStep FunctionsがCatchしたエラーの内容をそのままLambdaからSDKを叩いてSESで送信する部分を作ります。
SESの設定
SESはきちんとシステムとして利用しようと思ったら小難しい所があるかもしれませんが、試験用に利用する場合はサクッといけます。
流れとしては以下のような感じです。
- メールアドレス登録
- 登録メールアドレス確認
- テストメール送信
なお、本番環境で不特定多数へメールを送る場合には送信制限の解除申請が必要です。
詳細は下記をご参照下さい。
メールアドレス登録
SESの画面から「Email Address > Verify a New Email Address」を選択します。
通知したいメールアドレスを入力して「Verify This Email Address」でメールアドレスを登録します。
この段階では登録されただけで、ステータスが「pending verification」となって確認待ちのステータスです。
登録メールアドレス確認
登録したメールアドレス宛に確認用のメールが届きますので、リンクを24時間以内に押します。
ステータスが「verified」となり確認完了です。
テストメール送信
実際にメールを送ってみます。
登録したメールアドレスにチェックを入れて「Send a Test Email」を押します。
To:には登録したメールアドレス(From:と同じ)を、その他は適当に入れて「Send Test Email」を押します。
メールの受信が確認できます。
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」にします。
上記のコードを直接入力します。
ロールはLambdaからSESへアクセスしてメールを送信するものを作成するために「カスタムロールの作成」を選択します。
IAM Roleの作成画面が開きます。
ロール名を適当に入れ、「ポリシードキュメントを表示」を展開してから「編集」を押します。
確認画面が出るので「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のメール送信のみ許可する設定を追記しています。
編集したら「許可」を押します。
Lambdaの画面に戻り、「既存のロール」が作成したものになっていることを確認して「次へ」を押します。
確認画面になるので「関数の作成」から作成します。
作成が完了したら、「テスト」からいつもの「Hello World」を実行します。
成功したら、Hello Worldがメールで届きます。
これで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」を含んでいます。
入力したコードが正しいかは、「ビジュアルワークフロー」の更新を押すと大体わかります。
綺麗になっていれば少なくとも流れは大丈夫でしょう。
「ステートマシンの作成」から作成します。
ロールは前回作成したものをそのままで「OK」を押します。
それではテストしてみます。
現状では前回と変わらずに、「Sender」を経由しないで終了します。
エラーを発生させる
今回は無理やり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」では「成功」のステータスとなっています。
メールがちゃんと届くことも確認できます。
以上で、エラーの発生をメールで確認することが出来るようになりました。
確認後は、Update CheckerのLambdaはもとに戻します。
さいごに
Step Functionsを利用して、エラー発生時の処理を別のLambdaで定義して、簡単に繋げることが出来ました。
もちろん、エラー毎に違うLambdaを実行したり、簡単に入れ替えたり出来ます。(Step Functions自体は再度作成する必要がありますが…)
次回はアップデートをTwitterに流してみます。