1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-01-23

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に保管しておいたものを使用します。

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

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設定について

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でセキュリティグループと、サブネットの設定が必要。

- メールの構造

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からのデータ取り出し処理

index.py
  try:
    with connection.cursor() as cursor:
      cursor.execute(sql)
      result = cursor.fetchall()
    return result
  
  finally:
    connection.close()

connectionのcursor()メソッドを使用してデータをSQLに則り処理する。
fetchall()を使用しているので該当するデータをすべて取り出す。

- CSVファイルの保存

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ファイルの添付

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. 参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?