渡辺です。
S3バケットにあるファイルをメールで毎月1回送信すべしというお仕事が発生しました。
パッと思いついた実装方法は以下となります。
- CloudWatch Events ルールのスケジュール式 で毎月1回Lambda Function を動かす。
- Lambda Functionの実装
- S3バケットにある指定ファイルを取得する。
- SESのメールで添付ファイルとして送信する。
CloudWatch Eventsの部分は特に問題なかったのですが、LambdaでS3から添付ファイル付きメールをSESで送信するという部分で少しコツがいることがわかりました。
SES
まず、SESの部分です。
AWSのドキュメントにサンプルコードがあるので、それを利用しました。
毎度おなじみのPythonとboto3での実装例が載っています。
サンプルコードは上記の通り、2つありました。
1の方はsend_email()メソッドを使ったサンプル、2の方はsend_raw_email()メソッドを使ったサンプルとなっています。
添付ファイルをつけるには、send_raw_email()を使う必要があるようなので、2のサンプルコードを元にして開発してみました。
その前にsend_raw_email()のドキュメントを少しみてみます。
- verified email addresses or domainsからしかメール送信できません。
- もしAWSアカウントが、Amazon SES sandbox のままなら、verified email addressesか、 Amazon SES mailbox simulatorにしか送信できません。
今回は特定のメールアドレスのみへの送信でいいため、サンドボックスのまま利用します。
マネージメントコンソールでSESにメールアドレスを登録します。
すると、登録したメールアドレスに確認メールが来るので、そこに書かれたURLリンクをクリックするとverifidとなります。
- 添付ファイルを含む最大メッセージサイズは10MBです。
添付ファイルのサイズが大きい時などは要注意ですね。
今回はLambdaで圧縮処理し、サイズを小さくしてから、メールに添付します。
「え、S3保存した時点で圧縮した方がいい?ですよね。」
Lambda
次にサンプルコードから変更が必要な箇所をみていきます。
送信元アドレスと宛先アドレスの設定
# Replace sender@example.com with your "From" address.
# This address must be verified with Amazon SES.
SENDER = "Sender Name <sender@example.com>
"
# Replace recipient@example.com with a "To" address. If your account
# is still in the sandbox, this address must be verified.
RECIPIENT = "recipient@example.com
"
送信元アドレスと宛先アドレスは、SESでverifiedされたものに変更しましょう。
CONFIGURATION_SETの設定
# Specify a configuration set. If you do not want to use a configuration
# set, comment the following variable, and the
# ConfigurationSetName=CONFIGURATION_SET argument below.
CONFIGURATION_SET = "ConfigSet
"
SESの設定でConfiguration Setというのがあるのですが、それを指定します。
マネージメントコンソールで簡単に作成できるので、それを指定しても構いません。(設定値はデフォルトのままでOK)
または、サンプルコードの下の方にあるConfigurationSetName=CONFIGURATION_SET
をコメントアウトしても動きます。
S3オブジェクトをローカルに一時保存してから添付する
S3オブジェクトを添付ファイルとして扱いたいのですが、直接S3オブジェクトのURLやARNは指定ができません。
Lambda関数が動いているローカルマシンのファイルシステムに一時的に保存した後で、それを添付ファイルとして指定します。
但し、Lambdaには、書き込み可能なディレクトリが /tmp
のみという制限があります。
以下はs3オブジェクトをローカルに保存する例です。
例では、Bucket名/reports/yyyy/mm/xxxx.csv といった形で保存されているオブジェクトをdownload_fileで/tmpに保存し、zipfileで圧縮しています。
このコードはさておき、Lambdaでは/tmpにしか保存できないとだけ覚えておいていただければと思います。
# get last month today = datetime.datetime.today() thismonth = datetime.datetime(today.year, today.month, 1) lastmonth = thismonth + datetime.timedelta(days=-1) lastmonth_begin = lastmonth.strftime("%Y-%m") + '-01' lastmonth_end = lastmonth.strftime("%Y-%m-%d") # s3 location obj_path = f'reports/{lastmonth.strftime("%Y")}/{lastmonth.strftime("%m")}' s3 = boto3.resource('s3') bucket = s3.Bucket(Bucket名) for obj in bucket.objects.filter(Prefix=obj_path): basename = os.path.basename(obj.key) if re.search(r'\.csv$',basename): bucket.download_file(obj.key, '/tmp/' + basename) with zipfile.ZipFile('/tmp/report.zip', "w", zipfile.ZIP_DEFLATED) as zf: zf.write('/tmp/' + basename, "report.csv")
そして、保存したファイルをATTACHMENTで指定します。
# The full path to the file that will be attached to the email. ATTACHMENT = "/tmp/report.zip"</pre>
これで添付ファイル付きのメールを送信できるようになっています。
以下はAWS SDKs を使用して raw E メールを送信するを元にして作ったコードの全文となります。
(本題と関係ないため説明省略しましたが、datetimeとreとzipfileをimportしています。)
import os import boto3 import datetime import re import zipfile from botocore.exceptions import ClientError from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication def lambda_handler(event, context): # get last month today = datetime.datetime.today() thismonth = datetime.datetime(today.year, today.month, 1) lastmonth = thismonth + datetime.timedelta(days=-1) lastmonth_begin = lastmonth.strftime("%Y-%m") + '-01' lastmonth_end = lastmonth.strftime("%Y-%m-%d") # s3 location obj_path = f'reports/{lastmonth.strftime("%Y")}/{lastmonth.strftime("%m")}' s3 = boto3.resource('s3') bucket = s3.Bucket(Bucket名) for obj in bucket.objects.filter(Prefix=obj_path): basename = os.path.basename(obj.key) if re.search(r'\.csv$',basename): bucket.download_file(obj.key, '/tmp/' + basename) with zipfile.ZipFile('/tmp/report.zip', "w", zipfile.ZIP_DEFLATED) as zf: zf.write('/tmp/' + basename, "report.csv") # Replace sender@example.com with your "From" address. # This address must be verified with Amazon SES. SENDER = "XXX <xxx@example.co.jp>" # Replace recipient@example.com with a "To" address. If your account # is still in the sandbox, this address must be verified. RECIPIENT = "yyy@example.co.jp" # Specify a configuration set. If you do not want to use a configuration # set, comment the following variable, and the # ConfigurationSetName=CONFIGURATION_SET argument below. CONFIGURATION_SET = "ConfigSet" # If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES. AWS_REGION = "us-east-1" # The subject line for the email. SUBJECT = "Customer service contact info" # The full path to the file that will be attached to the email. ATTACHMENT = "/tmp/report.zip" # The email body for recipients with non-HTML email clients. BODY_TEXT = "Hello,\r\nPlease see the attached file for a list of customers to contact." # The HTML body of the email. BODY_HTML = """\ <html> <head></head> <body> <h1>Hello!</h1> <p>Please see the attached file for a list of customers to contact.</p> </body> </html> """ # The character encoding for the email. CHARSET = "utf-8" # Create a new SES resource and specify a region. client = boto3.client('ses',region_name=AWS_REGION) # Create a multipart/mixed parent container. msg = MIMEMultipart('mixed') # Add subject, from and to lines. msg['Subject'] = SUBJECT msg['From'] = SENDER msg['To'] = RECIPIENT # Create a multipart/alternative child container. msg_body = MIMEMultipart('alternative') # Encode the text and HTML content and set the character encoding. This step is # necessary if you're sending a message with characters outside the ASCII range. textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET) htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET) # Add the text and HTML parts to the child container. msg_body.attach(textpart) msg_body.attach(htmlpart) # Define the attachment part and encode it using MIMEApplication. att = MIMEApplication(open(ATTACHMENT, 'rb').read()) # Add a header to tell the email client to treat this part as an attachment, # and to give the attachment a name. att.add_header('Content-Disposition','attachment',filename=os.path.basename(ATTACHMENT)) # Attach the multipart/alternative child container to the multipart/mixed # parent container. msg.attach(msg_body) # Add the attachment to the parent container. msg.attach(att) #print(msg) try: #Provide the contents of the email. response = client.send_raw_email( Source=SENDER, Destinations=[ RECIPIENT ], RawMessage={ 'Data':msg.as_string(), }, #ConfigurationSetName=CONFIGURATION_SET ) # Display an error if something goes wrong. except ClientError as e: print(e.response['Error']['Message']) else: print("Email sent! Message ID:"), print(response['ResponseMetadata']['RequestId']) # Main Function if __name__ == "__main__": lambda_handler({}, {})
まとめ
- SESで添付ファイルが必要な時は、send_raw_emailメソッドを使う。
- SESで添付ファイルはs3オブジェクトを指定できないので、Lambdaに一時保存する。
- Lambdaは/tmpにしかファイルを保存できない。
渡辺 信秀(記事一覧)
2017年入社 / 地味な内容を丁寧に書きたい