AWSLambdaを使用してRDSからCSVを取得してメール送信

# 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に保管しておいたものを使用します。
```yaml:lambda.yaml
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
```python:index.py
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設定について
```yaml:lambda.yaml
- 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でセキュリティグループと、サブネットの設定が必要。

### - メールの構造
```python:index.py
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からのデータ取り出し処理
```python:index.py
  try:
    with connection.cursor() as cursor:
      cursor.execute(sql)
      result = cursor.fetchall()
    return result
  
  finally:
    connection.close()
```
connectionのcursor()メソッドを使用してデータをSQLに則り処理する。
fetchall()を使用しているので該当するデータをすべて取り出す。

### - CSVファイルの保存
```python:index.py
# 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ファイルの添付
```python:index.py
# メールに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. 参考資料

https://qiita.com/kobayashi_0226/items/d8f97c652873d80b1367

https://qiita.com/ma2kazu/items/2a2c69743963f06cdacf

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-email-raw.html

https://retheviper.github.io/posts/python-blob-to-binary/

https://ja.stackoverflow.com/questions/93162/aws%E3%81%A7lambda%E9%96%A2%E6%95%B0%E3%81%8B%E3%82%89rds%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84