본문 바로가기

AWS/Auto-Report

Auto Report #1 Lambda & EventBridge

반응형

우선 Lambda 를 생성하고 EvetnBridge 를 Trigger 로 설정하여 스케쥴링을 할 예정입니다.

Lambda 를 생성하다 보니 Lambda Function 자체적으로 URL 을 할당받을 수 있어서 API Gateway 를 사용안하기로 했습니다.

단순 데이터를 받으려고 했고 API Gateway 사용이 필요할만한 작업은 아닙니다. 또 IAM (권한) 에 대해선 따로 작성하지 않았습니다. 이번에 생성한 Lambda 는 S3의 템플릿과 SES 서비스를 호출해야해서 두 서비스에 대한 IAM 만 부여했으니 참고해주세요.

 

1. Lambda Function 생성

AWS Lambda Function

우선 Lambda Fuction 을 생성하고 Trigger 로 EventBridge 를 선택합니다. 위에 사진에서 Layers 가 (1) 인 이유는 문서 수정에 필요한 라이브러리가 Lambda Fuction 에서 제공하지 않아서 입니다. Lambda 에서 제공하지 않은 라이브러리는 Layers 에 등록한 후 사용가능합니다. 이번 포스팅에서는 문서 수정하는것을 다루지 않으니 참고해주세요

 

2. EventBridge 생성

 

AWS EventBridge

다음으로는 EventBridge 를 설정해줍니다. 저는 Cron 방식으로 설정했는데 리눅스 cron 과 동일한 형태이긴 하지만 약간 차이가 있습니다. 우선 기본적인 순서는 [ 분, 시간, 일, 월, 요일, 년] 으로 설정하게 되어 있습니다. 리눅스와 다르게 연도도 설정가능합니다.

그 다음으로 입력할 수 있는 데이터 입니다. 리눅스와 동일하게 숫자 혹은 * (와일드카드) 로 설정가능합니다. 하지만 ? 가 추가되어 약간 혼란스러웠습니다. ? 는 언제일지 모를때(?) 사용합니다. 예를들어 그림의 내용은 3일 10시 10분 에 이벤트가 발생할 텐데 3일이 무슨요일인지는 매달 다를겁니다. 때문에 Day of week 항목에는 ? 를 넣어줘야합니다. 

 정상적으로 입력이 되었다면 하단에 언제수행되는지 보입니다. 혹시 보이지 않는다면 잘못 입력을 하였으니 다시 입력을 해봐야합니다.

 

3. Lambda Code

 

 

import json
import base64
import io
import os
from tempfile import NamedTemporaryFile
from botocore.exceptions import ClientError
import botocore
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import boto3
from datetime import datetime, timedelta
from openpyxl import load_workbook
from openpyxl.styles import Alignment

s3 = boto3.resource('s3')

def download_file(bucket, key):
    try:
        file = NamedTemporaryFile(suffix = '.xlsx', delete=False)
        bucket = s3.Bucket(bucket)
        response = bucket.download_file(key, file.name)
        return file.name
    except ClientError as e:
        if e.response['Error']['Code'] == "404":
            return None
        else:
            raise
    else:
        raise
        
def upload_file(file_name, bucket, object_name=None):
    if object_name is None:
        object_name = os.path.basename(file_name)
    s3_client = boto3.client('s3')
    try:
        response = s3_client.upload_file(file_name, bucket, object_name)
    except ClientError as e:
        logging.error(e)
        return False
    return True

def lambda_handler(event, context):
    # MAIL Config
    SENDER = "Sender Email Address"
    RECIPIMENT = "Receiver Email Address"
    AWS_REGION = "ap-northeast-2"
    CHARSET = "UTF-8"
    SUBJECT = "Subject"
    client = boto3.client('ses', region_name=AWS_REGION)
    
    # EventBridge Schedule 로 동작할 때
    try:
        if event["detail-type"] == "Scheduled Event":
            BODY_HTML = """
            <html>
            <head></head>
            <body>
            <h3>주간 보고</h3>
            <h5>업무시간이 52시간 인 경우 '메일 보내기', 시간을 직접 입력하실 경우 '다시 작성하기'를 눌러주세요</h5>
            </br>
            <p><a href="https://[Lambdafunction URL]/" style="text-decoration: none; color:#555">메일 보내기</a></p>
            <p><a href="https://[S3 WebHosting URL]" style="text-decoration: none; color:#555">다시 작성하기</a></p>
            </body>
            </html>
            """
            response = client.send_email(
                Destination = {
                    'ToAddresses': [
                        RECIPIMENT,
                    ],
                },
                Message = {
                    'Body' : {
                        'Html' : {
                            'Charset' : CHARSET,
                            'Data' : BODY_HTML,
                        }
                    },
                    'Subject':{
                        'Charset': CHARSET,
                        'Data' : SUBJECT,
                    },
                },
                Source=SENDER,
                )
            return '메일함을 확인해주세요'
        else:
            return event
            
    # URL 호출을 받아 동작할때
    except:
        file = download_file([Bucket Name], [Object key])
        today = datetime.today()
        excel_document = load_workbook(file)
        
        try:
        
        # 입력값이 있을경우
            if event["body"]:
                decode_dt = base64.b64decode(event["body"])
                str_dt = decode_dt.decode('ascii')
                rate_data = str_dt.split('=')[1]
                # 입력받은 데이터를 통해 엑셀 수정 후 저장 
                excel_document.save(file)
        # 입력값이 없을 경우         
        except:
        	# 기본 데이터(52)를 통해 엑셀 수정 후 저장
            rate_data = '52'
            excel_document.save(file)

        try:
        
        	# 헤더의 sec-fetch-mode 가 navigate 인 경우 수정한 엑셀 파일을 첨부하여 메일 송부
            
            if event["headers"]["sec-fetch-mode"] == "navigate":
                msg = MIMEMultipart('mixed')
                
                msg['Subject'] = SUBJECT
                msg['From'] = SENDER
                msg['To'] = RECIPIMENT
                
                msg_body = MIMEMultipart('alternative')
                
                BODY_HTML = """
                <html>
                <head></head>
                <body>
                <h3>주간 업무시간 보고</h3>
                </body>
                </html>
                """
                textpart = MIMEText(BODY_HTML.encode(CHARSET), 'plain', CHARSET)
                htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET)
                
                msg_body.attach(textpart)
                msg_body.attach(htmlpart)
                
                att = MIMEApplication(open(file, 'rb').read())
                att.add_header('Content-Disposition', 'attachment', filename=os.path.basename([첨부파일 명]))
                
                msg.attach(msg_body)
                msg.attach(att)
                
                response = client.send_raw_email(
                    Source=SENDER,
                    Destinations=[RECIPIMENT],
                    RawMessage = {
                        'Data':msg.as_string()
                    }
                    )
                upload_file(file, [Bucket Name], [Object key])
        except ClientError as e:
            return e.response['Error']['Message']
        else:
            with open('return.html', 'r', encoding='utf-8') as h:
                content = h.read()
            return {
                "statusCode": 200,
                "headers": {
                    'Content-Type': 'text/html'
                    },  
                "body": content
            }

 

 

 

- Library

 

  1. base64, io : html form 에서 요청받은 데이터가 2 데이터 이기 때문에 데이터를 인코딩 하기 위해 사용
  2. os, tempfile : S3 에서 파일을 읽을  임시파일로 저장하기 위해 사용
  3. botocore, boto3 : AWS 서비스들에 요청을 보내기 위해 사용
  4. email : Email  보낼  단순 메세지가 아니라 첨부파일이 포함된 메세지를 보내기 위해 사용
  5. datetime : 시간 데이터를 다루기 위해 사용
  6. openpyxl : 엑셀(첨부파일)  다루기 위해 사용, 기본적으로 제공되지 않아 Layer  등록해서 사용해야 

 

 

- download_file 함수 : 버킷에서 기존 엑셀파일을 다운로드 받기 위한 함수

 

- upload_file 함수 : 다운로드 받은 함수를 수정  S3  업로드 하기 위한 함수

 

- Lambda_handler

 

  1. #EventBridge Schedule  동작할때 : 이벤트브릿지 스케쥴로 동작할때는 event  데이터  detail-type Scheduled Event  전달받는다  경우에는 첨부파일을 첨부하지 않고 정규적인 내용의 데이터를 보낸다. 정규적인 내용에는 주기적으로 받는 데이터인지 (52시간인지?) 혹은 데이터의 수정이 필요한지를 묻는 내용이 포함되어 있다.
  2. # URL 호출을 받아 동작할  :  경우 download_file 함수를 활용해 기존 엑셀파일을 다운로드를 받고 엑셀을수정하여 메일에 첨부한  보내게 된다. 이때 입력값이 있을 경우 (데이터 수정이 필요할때) 혹은 입력값이 없을경우 (데이터 수정이 필요없을때)  나누어 엑셀파일을 수정한  메일을 보내게 된다.
  3. 기타 : 마지막에 헤더부분의 sec-fetch-mode  navigate  경우에만 메일을 보내게 조건문을 달았다. 처음엔 부분을 하지 않았는데 lambda  호출하는 html form 에서 post 메소드를 사용하여 호출하게 되면 보안상cors (교차 출처 리소스 공유)  통해 입력 데이터의 출처를 체크한다. 이때 1번의 post  1번의 get  받기 때문에 요청은 1번이였지만 메일은 2 발송하게 된다. 따라서 1번의 post  get  식별하기 위해  부분을 작성하였다. request 데이터를 비교해 다른 부분을 작성하였으니  부분은 메소드 값으로 구분해도 무방하다.

- Return :

마지막 return 부분에서 headers  content-Type text/html  보내고 람다에 만들어뒀던 return.html  불러와서 body  넣어 보내면 return.html  응답을 받게 된다.



 

반응형

'AWS > Auto-Report' 카테고리의 다른 글

Auto Report (using AWS Lambda, SES)  (0) 2023.01.21