1.はじめに
はじめまして!株式会社TechoesインフラチームのYです!
今年の4月で2年目を迎える初心者のクラウドエンジニアです。
今回からTechoesの採用強化、学習内容のアウトプットを目的としてQiitaで技術ブログを書いていこうと思います。
第1回目として最近学習したAWSLambdaを使ってRDSからCSVを取得してメール送信する手順を投稿します。
2.前提
- 言語:Python 3.10
- ライブラリ:pymysql
※layerとしてS3に保管 - RDS:Aurora MySQL
- RDSの認証情報はSecretManagerから取得
- メール送信にはSESを使用
3. CFNテンプレート
Lambda関数を作成・管理するテンプレートです。
レイヤーはあらかじめS3に保管しておいたものを使用します。
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
RoleName: sample-Lambda-Role
Policies:
- PolicyName: LambdaDefaultPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- PolicyName: LambdaPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
- secretsmanager:GetSecretValue
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DetachNetworkInterface
- ec2:DeleteNetworkInterface
Resource: "*"
PythonModulesLayer:
Type: AWS::Lambda::LayerVersion
Properties:
Description: python modules Layer
CompatibleArchitectures:
- arm64
- x86_64
CompatibleRuntimes:
- python3.10
Content:
S3Bucket: lambda-layer-bucket
S3Key: layer.zip #Layer File
LayerName: lambdalayer
RDSToSESFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: mylambdafunction
Description: sample
Environment:
Variables:
'region': ap-northeast-1
'env': sample
'SENDER_MAIL': sender@sample.com
'RECIPIENT_MAIL': recipient@sample.com
'CC_MAIL': cc1@sample.com,cc2@sample.com
VpcConfig:
SecurityGroupIds:
- "**********"
SubnetIds:
- "**********"
- "**********"
Runtime: python3.10
Role: !GetAtt LambdaRDSRole.Arn
Handler: index.lambda_handler
Layers:
- !Ref PythonModulesLayer
MemorySize: 128
Timeout: 60
Code:
ZipFile: |
4. Python
import boto3
import json
import os
import pymysql.cursors
import csv
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
# SES
SENDER_MAIL = os.environ['SENDER_MAIL']
RECIPIENT_MAIL = os.environ['RECIPIENT_MAIL']
CC_MAIL = os.environ['CC_MAIL']
cc_mail_list = CC_MAIL.split(',')
SUBJECT = "メールの件名"
BODY = "メールの本文"
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
PROJECT_ENV = os.environ['env']
REGION = os.environ['region']
# Secrets Manager
secret_name = 'Secrets Managerに登録されているシークレット名'
secrets_manager = boto3.client('secretsmanager')
secret = secrets_manager.get_secret_value(SecretId=secret_name)
rds_credentials = json.loads(secret['SecretString'])
# SES
ses_client = boto3.client('ses', region_name=REGION)
try:
message = MIMEMultipart()
message['Subject'] = SUBJECT
message['From'] = SENDER_MAIL
message['To'] = RECIPIENT_MAIL
for cc_mail in cc_mail_list:
message['Cc'] = cc_mail
text_part = MIMEText(BODY.encode('utf-8'), 'plain', 'utf-8')
message.attach(text_part)
# SQL
sql = "SELECT *FROM example;"
result = execute_sql(rds_credentials, sql)
csv_file_path = '/tmp/example.csv'
save_to_csv(result, csv_file_path)
attach_csv_to_email(message, csv_file_path, 'example.csv')
# send
destinations=[RECIPIENT_MAIL] + cc_mail_list
response = ses_client.send_raw_email(
Source=SENDER_MAIL,
Destinations=destinations,
RawMessage={'Data': message.as_string()}
)
print("メールが送信されました")
except Exception as e:
logger.error(f"Error: {e}")
# sql実行
def execute_sql(rds_credentials, sql):
connection = pymysql.connect(
host=rds_credentials['host'],
user=rds_credentials['username'],
password=rds_credentials['password'],
database=rds_credentials['dbname'],
port=rds_credentials['port'],
cursorclass=pymysql.cursors.DictCursor
)
try:
with connection.cursor() as cursor:
cursor.execute(sql)
result = cursor.fetchall()
return result
finally:
connection.close()
# メールにCSVを添付
def attach_csv_to_email(message, csv_file_path, filename):
with open(csv_file_path, 'r') as attachment:
csv_part = MIMEApplication(attachment.read(), 'csv')
csv_part.add_header('Content-Disposition', f'attachment; filename={filename}')
message.attach(csv_part)
# csv保存
def save_to_csv(data, file_path):
csv_data = []
for row in data:
csv_data.append(','.join(map(str, row.values())))
with open(file_path, 'w', newline='') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(data[0].keys())
for row in data:
csv_writer.writerow(row.values())
5. 細かい部分の言語化
- LambdaのVPC設定について
- PolicyName: LambdaPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
- secretsmanager:GetSecretValue
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DetachNetworkInterface
- ec2:DeleteNetworkInterface
#//省略
VpcConfig:
SecurityGroupIds:
- "**********"
SubnetIds:
- "**********"
- "**********"
RDSはVPC内に配置されるサービス。LambdaはVPC外に配置されるサービス。
LambdaからRDSにアクセスするためにはLambdaにVPCを割り当てる必要があるため、ポリシーとLambdaでセキュリティグループと、サブネットの設定が必要。
- メールの構造
message = MIMEMultipart()
message['Subject'] = SUBJECT
message['From'] = SENDER_MAIL
message['To'] = RECIPIENT_MAIL
for cc_mail in cc_mail_list:
message['Cc'] = cc_mail
メールの構造をMimeMultipartを作成して設定する。
CCの部分はfor文を使うことによってlistの要素が順番に代入され、複数のアドレスに送信できる。
- RDSからのデータ取り出し処理
try:
with connection.cursor() as cursor:
cursor.execute(sql)
result = cursor.fetchall()
return result
finally:
connection.close()
connectionのcursor()メソッドを使用してデータをSQLに則り処理する。
fetchall()を使用しているので該当するデータをすべて取り出す。
- CSVファイルの保存
# csv保存
def save_to_csv(data, file_path):
csv_data = []
for row in data:
csv_data.append(','.join(map(str, row.values())))
with open(file_path, 'w', newline='') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(data[0].keys())
for row in data:
csv_writer.writerow(row.values())
取得した値をstringに変換し、カンマを追加。
これをcsv_dataに格納。
ヘッダ行として列の名前を書き込む。
dataの各行の値をCSVファイルに書き込む。
- CSVファイルの添付
# メールにCSVを添付
def attach_csv_to_email(message, csv_file_path, filename):
with open(csv_file_path, 'r') as attachment:
csv_part = MIMEApplication(attachment.read(), 'csv')
csv_part.add_header('Content-Disposition', f'attachment; filename={filename}')
message.attach(csv_part)
メソッドが呼び出されると保存されているcsvファイルをMIME形式で構築してメールに添付する。
6. 最後に
自分では理解しているつもりでも、いざ言語化してみると難しいと感じました。
まだまだ理解が浅い部分もあるので記事に対して気になる点がありましたら教えていただけると幸いです。
以上、株式会社TechoesインフラチームのYでした!
7. 参考資料