일일 AWS 비용 알림 설정(Python, Lambda)

해당 가이드는 AWS 일일 빌링 알림을 위한 설정을 위한 설정 및 Lambda 코드 배포에대해 다룹니다.
여러 계정의 빌링 또한 알림으로 받아볼 수 있습니다.

코드는 계속 업데이트 됩니다.

소스는 여기에서 확인하실 수 있습니다.


들어가기 전에

본 포스트에는 아래 구조를 통하여 알림이 전송됩니다.

  • AWS EventBridge: cron을 통하여 매일 이벤트를 생성하여 Lambda를 트리거하는 역할을 맡습니다.
  • AWS Secrets Manager: 빌링 데이터를 조회하기 위한 각 IAM 사용자의 Secrets Key(시크릿키)를 저장하는 역할을 맡습니다.
  • Google Sheets(스프레드시트): 알림(Email, Slack)을 정의하는 데이터를 가지고 있으며 Lambda 함수에 의해 호출됩니다.
  • AWS Cost Explorer: AWS에 저장된 빌링 정보를 가지고있으며 Lambda 함수에 의해 호출됩니다.
  • AWS Lambda:
    1. Event Bridge에 의해 매일 함수가 트리거됩니다.
    2. 이후 Google Sheets와 Secrets Manager에 저장해둔 데이터를 가져오며 몇가지 조건이 있습니다.
    2.1 Secrets Manager와 Google Sheets에 입력한 AWS Account의 개수가 동일해야합니다.
    2.2 Secrets Manager에 저장하는 시크릿키는 Account의 별도의 계정을 생성하여 IAM 계정을 부여해야합니다.
    3. Google Sheets에 알림을 켜둔대상으로 Slack 또는 Email로 알림을 전송합니다.
스프레드시트와 AWS Secrets Manager를 분리한 이유는?

계정의 추가의 경우 스프레트시트와 AWS Secrets Manager 모두 작성해야하지만 유지보수를 할때는 보통 알림 수신여부 또는 수신대상이 변경되는 작업이 많아 해당작업은 스프레드시트에서 처리합니다.

IAM 사용자의 시크릿키를 보관하기에는 스프레드시트는 보안상 적합하지않아 AWS Secrets Manager에 보관합니다.

+ 메일 Provider는 gmail을 사용하며 메일을 발신할 계정은 stmp를 통해 이메일 전송이 가능한 상태여야합니다.


1. IAM 계정 및 Secrets Key 발급

알림받을 계정에 IAM 계정을 생성합니다.
정책은 아래와 같이 설정합니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ce:*"
            ],
            "Resource": "*"
        }
    ]
}

2. AWS Secret Manager 생성

Secret Key는 2가지를 생성합니다.

  1. email password
  2. AWS IAM User secret key

Secrets Manager은 Lambda를 실행할 계정에 권한이 있어야합니다.
AWS Organization Root IAM 계정 또는 아래와 같이 리소스의 접근권한이 있는 계정이어야합니다.

Allow: secretsmanager:GetSecretValue

1. email password

AWS Secrets Manager 보안 암호 생성시 “일반 텍스트” 타입으로 설정합니다.

# 패스워드만 입력(패스워드 이외의 공백 x)

password1!

2. AWS IAM User secret key

AWS Secrets Manager 보안 암호 생성시 “일반 텍스트” 타입으로 설정합니다.

아래와 같이 데이터를 Python List 형태로 dict를 작성합니다.

[
	{
		"Customer": "ABC Company",
		"AWS_ACCOUNT_ID": "",
		"ACCESS_KEY": "",
		"SECRET_ACCESS_KEY": ""
	},
	{
		"Customer": "DEF Company",
		"AWS_ACCOUNT_ID": "",
		"ACCESS_KEY": "",
		"SECRET_ACCESS_KEY": ""
	}
	...
]
리소스 권한, 교체 구성은 별도로 필요하지 않습니다.
(교체구성 → 비활성화)

3. Google Sheets 작성 및 Google API 설정

Google Sheet는 1개 파일에 2개의 시트를 사용합니다.

먼저, 수신 대상 시트입니다.
(시트이름은 “info” 입니다.)

다음은 관리자 전용 시트입니다.

(시트이름은 “admin” 입니다.)

관리자(admin)의 알림은 알림 수신여부, 알림 플랫폼, 전체 알림 내용을 전달 받습니다.

시트 작성이 완료되었으면 Python에서 데이터를 가져올 수 있게 API 설정을 해줍니다.

GCP(Google Cloud Platform)에 접속 후 프로젝트를 생성합니다.

google sheet api를 검색하여 MARKETPLACE에 있는 Google Sheets API를 클릭하여 사용으로 설정합니다.

프로젝트로 이동하여 사용자 인증 정보를 만들어줍니다.
(서비스 계정 사용)

생성시 아래와 같이 프로젝트에 대한 액세스 권한을 할당해줍니다.
기본 - 뷰어
기본 - 소유자
기본 - 탐색자

IAM 및 관리자” 메뉴로 이동하여 서비스 계정에 대한 키를 추가합니다.
(JSON 형태로 만들어줍니다.)

생성이 완료되면 PC에 다운로드가 됩니다.
이때 파일 이름을 “gcp_credentials.json”으로 변경합니다.

사용자 계정이 생성되면 만들어 두었던 시트를 확인가능하게끔 스프레드시트의 파일에 권한을 부여해줍니다.
(Google Sheets 파일에 들어가서 우측상단 “공유” 버튼을 눌러 추가할 수 있습니다.)


4. Lambda 함수 작성

함수 작성에 필요한 부분은 2가지입니다.

  1. code
  2. Layer

1. Lambda Layer 추가

AWS의 Layer는 라이브러리가 저장되는 계층을 의미하며 AWS에서 기본적으로 제공하는 라이브러리 외에 별도의 라이브러리 사용시 Layer를 추가해야합니다.

본 포스트에 사용할 라이브러리는 **gspread**를 사용하며 pip를 사용합니다.

사용할 라이브러리를 특정 패키지에 다운로드

# pip update && pip install --target=<다운로드될 위치> gspread
pip install --target=./ gspread

폴더 이름을 python으로 변경하고 zip으로 압축합니다.
(Layer 등록시 파일명이 python.zip이 아니면 인식을 못합니다.)

zip python.zip python

python.zip을 Layer에 추가합니다.
AWS Lambda → 계층(Layer) → Add Layer → python.zip 업로드 → 호환 런타임(python3.7 python3.8)

2. IAM Role 추가

이후 Lambda 함수가 사용할 IAM 역할을 생성합니다.
정책은 아래와 같습니다

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-2:<AWS Account ID>:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-2:<AWS Account ID>:log-group:/aws/lambda/<Lambda 함수 이름>:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "arn:aws:secretsmanager:*:<AWS Account ID>:secret:*"
        }
    ]
}

3. Lambda 함수 생성 및 작성

이제 Lambda 함수를 생성합니다.

런타임: Python 3.8(추천, 3.7 3.9 상관없음)
아키텍처: x86_64(상관없음)
실행 역할: 이전에 방금 전 생성한 역할 할당

생성 후 Layer를 추가합니다.

workspace에 이전에 발급받은 gcp_credentials.json을 업로드합니다.

lambda_function.py를 작성합니다.
(<> 안에 들어갈 데이터를 환경에 맞게 입력합니다.)

import json
import boto3
import urllib
import gspread
import smtplib, ssl
from botocore.exceptions import ClientError
from typing import Optional, Tuple
from datetime import date, timedelta
from decimal import Decimal
from traceback import format_exc
from oauth2client.service_account import ServiceAccountCredentials
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header

def lambda_handler(event, context):
# def run():

    # 이메일 발신자 정의
    SENDER_EMAIL = "<Email>"
    EMAIL_SECRET_ARN = "<email password Secret ARN>"
    SENDER_PASSWORD = str(get_email_secret(EMAIL_SECRET_ARN))
    
    # AWS Secret Manager의 데이터 가져오기
    CUSTOMER_SECRET_ARN = "<AWS IAM User secret key Secret ARN>"
    SECRET_DATA = get_customer_secret(CUSTOMER_SECRET_ARN)
    
    # Google Spread Sheet의 데이터 가져오기
    DATA_SHEET, ADMIN_DATA_SHEET = get_spread_sheet_data()

    # 모든 stdout을 관리자에게 전송할 데이터입니다.
    admin_logs = ""

    for customer in range(len(SECRET_DATA)):
        CUSTOMER = SECRET_DATA[customer]["Customer"]
        ACCOUNT = SECRET_DATA[customer]["AWS_ACCOUNT_ID"]
        ACCESS_KEY = SECRET_DATA[customer]["ACCESS_KEY"]
        SECRET_KEY = SECRET_DATA[customer]["SECRET_ACCESS_KEY"]
        
        result = {"daily": None, "monthly": None, "premonth": None}
        
        ce = boto3.client(
            'ce',
            aws_access_key_id=ACCESS_KEY,
            aws_secret_access_key=SECRET_KEY,
        )
        
        for key, date_range in {
            "daily": get_daily_range(),
            "monthly": get_current_month_range(),
            "premonth": get_pre_month_range(),
        }.items():
            try:
                result[key] = {"isSuccess": True, "data": get_cost(ce, date_range)}
            except Exception as e:
                result[key] = {
                    "isSuccess": False,
                    "error": {"message": str(e), "stacktrace": format_exc()},
                }

        admin_logs = admin_logs + "\n=============================\n"

        # Secret Manager와 Spread Sheet에 있는값을 서로 검사하여 알림 발송
        # Secret Manager에 등록된 Account 개수만큼 반복
        for alert_count in range(len(SECRET_DATA)):
            try:
                if ACCOUNT != DATA_SHEET[alert_count][0].get("AWS Account ID"):
                    alert_count = alert_count + 1
                elif ACCOUNT == DATA_SHEET[alert_count][0].get("AWS Account ID") and DATA_SHEET[alert_count][0].get("Notification") == "TRUE":
                    admin_logs = admin_logs + f"\n*{DATA_SHEET[alert_count][0].get('Customer')}*  |  AWS Secret Manager({ACCOUNT}) is match Spread Sheet({DATA_SHEET[alert_count][0].get('AWS Account ID')})\n"
                    # email 포함한 모든 알림 전송
                    if DATA_SHEET[alert_count][0].get("Notification Format").find("slack") != -1 and DATA_SHEET[alert_count][0].get("Notification Format").find("email") != -1:
                        admin_logs = admin_logs + "\nNotification Type: slack & email\n"
                        admin_logs = admin_logs + "\n----Slack MESSAGE----\n"
                        slack_message = create_slack_message(result, CUSTOMER, ACCOUNT) 
                        admin_logs = admin_logs + slack_message
                        admin_logs = admin_logs + "\n----Email MESSAGE----\n"
                        email_message = create_email_message(result, CUSTOMER, ACCOUNT)
                        admin_logs = admin_logs + email_message
                        webhook_url = DATA_SHEET[alert_count][0].get("webhook url for alert")
                        to_email = DATA_SHEET[alert_count][0].get("email for alert")
                        post_to_slack(message=slack_message,url=webhook_url)
                        post_to_email(sender=SENDER_EMAIL,sender_password=SENDER_PASSWORD,message=email_message,email=to_email,subject="[eocis.app] 일일 AWS 비용 알림")
                        break
                    # Slack 알림 전송
                    elif DATA_SHEET[alert_count][0].get("Notification Format").find("slack") != -1:
                        admin_logs = admin_logs + "\nNotification Type: slack\n"
                        admin_logs = admin_logs + "\n----MESSAGE----\n"
                        slack_message = create_slack_message(result, CUSTOMER, ACCOUNT)        
                        admin_logs = admin_logs + slack_message
                        webhook_url = DATA_SHEET[alert_count][0].get("webhook url for alert")
                        post_to_slack(message=slack_message,url=webhook_url)
                        break
                    # Email 알림 전송
                    elif DATA_SHEET[alert_count][0].get("Notification Format").find("email") != -1:
                        admin_logs = admin_logs + "\nNotification Type: email\n"
                        admin_logs = admin_logs + "\n----MESSAGE----\n"
                        email_message = create_email_message(result, CUSTOMER, ACCOUNT)
                        admin_logs = admin_logs + email_message
                        to_email = DATA_SHEET[alert_count][0].get("email for alert")
                        post_to_email(sender=SENDER_EMAIL,sender_password=SENDER_PASSWORD,message=email_message,email=to_email,subject="[eocis.app] 일일 AWS 비용 알림")
                        break
                    else:
                        admin_logs = admin_logs + f"\n*{DATA_SHEET[alert_count][0].get('Customer')}* |  ERROR 1: 알림 전송 플랫폼 정의 오류\n"
                        break
                elif ACCOUNT == DATA_SHEET[alert_count][0].get("AWS Account ID") and DATA_SHEET[alert_count][0].get("Notification") == "FALSE":
                    admin_logs = admin_logs + f"\n{DATA_SHEET[alert_count][0].get('Customer')}  |  AWS Secret Manager({ACCOUNT}) is match Spread Sheet({DATA_SHEET[alert_count][0].get('AWS Account ID')})\n"
                    admin_logs = admin_logs + f"\nINFO_{DATA_SHEET[alert_count][0].get('Customer')}: 알림 비활성화\n"
                    break
                else:
                    admin_logs = admin_logs + f"\n`{DATA_SHEET[alert_count][0].get('Customer')}` |  ERROR 2: 데이터간 정합성 확인 오류\n"
            except Exception as e:
                for send in range(len(ADMIN_DATA_SHEET)):
                    if ADMIN_DATA_SHEET[send][0].get("Notification") == "TRUE":
                        if ADMIN_DATA_SHEET[send][0].get("Notification Format") == "slack":
                            webhook_url = ADMIN_DATA_SHEET[send][0].get("webhook url for alert")
                            post_to_slack(message=f'{DATA_SHEET[alert_count][0].get("AWS Account ID")} error\n {e}',url=webhook_url)
                        elif ADMIN_DATA_SHEET[send][0].get("Notification Format") == "email":
                            to_email = ADMIN_DATA_SHEET[send][0].get("email for alert")
                            post_to_email(message=f'{DATA_SHEET[alert_count][0].get("AWS Account ID")} error',email=to_email)
                        elif "slack" in ADMIN_DATA_SHEET[send][0].get("Notification Format") and "email" in ADMIN_DATA_SHEET[send][0].get("Notification Format"):
                            webhook_url = ADMIN_DATA_SHEET[send][0].get("webhook url for alert")
                            to_email = ADMIN_DATA_SHEET[send][0].get("email for alert")
                            post_to_slack(message=f'{DATA_SHEET[alert_count][0].get("AWS Account ID")} error\n {e}',url=webhook_url)
                            post_to_email(message=f'{DATA_SHEET[alert_count][0].get("AWS Account ID")} error\n {e}',email=to_email)
                        else:
                            admin_logs = admin_logs + "\nERROR 3: 관리자 정보 에러\n"
                    else:
                        admin_logs = admin_logs + f"INFO_{ADMIN_DATA_SHEET[send][0].get('Receiver')}_Notification : OFF"
                        pass
    for send in range(len(ADMIN_DATA_SHEET)):
        if ADMIN_DATA_SHEET[send][0].get("Notification") == "TRUE":
            if ADMIN_DATA_SHEET[send][0].get("Notification Format") == "slack":
                webhook_url = ADMIN_DATA_SHEET[send][0].get("webhook url for alert")
                post_to_slack(message=admin_logs,url=webhook_url)
            elif ADMIN_DATA_SHEET[send][0].get("Notification Format") == "email":
                to_email = ADMIN_DATA_SHEET[send][0].get("email for alert")
                post_to_email(message=admin_logs,email=to_email)
            elif "slack" in ADMIN_DATA_SHEET[send][0].get("Notification Format") and "email" in ADMIN_DATA_SHEET[send][0].get("Notification Format"):
                webhook_url = ADMIN_DATA_SHEET[send][0].get("webhook url for alert")
                to_email = ADMIN_DATA_SHEET[send][0].get("email for alert")
                post_to_slack(message=admin_logs,url=webhook_url)
                post_to_email(message=admin_logs,email=to_email)
            else:
                print("\nERROR 3: 관리자 정보 에러\n")
        else:
            print(f"INFO_{ADMIN_DATA_SHEET[send][0].get('Receiver')}_Notification : OFF")
            pass    
    return result
    
def get_email_secret(secret_arn: str):

    region_name = "ap-northeast-2"

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
    secret = get_secret_value_response['SecretString']
    
    return secret
    
def get_customer_secret(secret_arn: str):

    region_name = "ap-northeast-2"

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
    key = get_secret_value_response['SecretString']
    key = key.replace("\n","")
    key = key.replace(" ","")
    secret = json.loads(key)
    
    return secret

def get_spread_sheet_data():
    spreadsheet_url = '<스프레드시트 URL>'
    JSON_KEY = 'gcp_credentials.json'

    scope = [
        'https://spreadsheets.google.com/feeds',
        'https://www.googleapis.com/auth/drive'
    ]

    credentials = ServiceAccountCredentials.from_json_keyfile_name(JSON_KEY, scope)
    googleCredentials = gspread.authorize(credentials)
    customerListDocs = googleCredentials.open_by_url(spreadsheet_url)
    customerListDocsSheet = customerListDocs.worksheet('info')
    adminListDocsSheet = customerListDocs.worksheet('admin')

    # info 시트 내용 가져오기
    allCustomerData = list()
    customerValues = customerListDocsSheet.get_all_values()
    # 카테고리가 있는 순서(AWS Account ID 등)
    customerCategory = 0

    for value in range(len(customerValues)):
        locals()['tmp_dict'] = dict()
        locals()['tmp_list'] = list()
        if value <= customerCategory:
            pass
        else:
            for cell in range(len(customerValues[customerCategory])):
                locals()['tmp_dict'][customerValues[customerCategory][cell]] = customerValues[value][cell]
            locals()['tmp_list'].append(locals()['tmp_dict'])
            allCustomerData.append(locals()['tmp_list'])
    
    # index-error 시트 내용 가져오기
    allAdminData = list()
    adminValues = adminListDocsSheet.get_all_values()
    # 카테고리가 있는 순서(AWS Account ID 등)
    adminCategory = 1

    for value in range(len(adminValues)):
        locals()['tmp_dict'] = dict()
        locals()['tmp_list'] = list()
        if value <= adminCategory:
            pass
        else:
            for cell in range(len(adminValues[adminCategory])):
                locals()['tmp_dict'][adminValues[adminCategory][cell]] = adminValues[value][cell]
            locals()['tmp_list'].append(locals()['tmp_dict'])
            allAdminData.append(locals()['tmp_list'])

    return allCustomerData, allAdminData
    
#################

def post_to_slack(message: str, url: str):
    data = {"text": message}
    req = urllib.request.Request(
        url,
        json.dumps(data, ensure_ascii=False).encode(),
        {"Content-Type": "application/json"},
    )
    resp = urllib.request.urlopen(req)
    return resp

def post_to_email(sender: str, sender_password: str, message: str, email: str, subject: str):
    SMTP_SSL_PORT=465 # SSL connection
    SMTP_SERVER="smtp.gmail.com"

    send_message = MIMEMultipart()
    send_message['From'] = sender
    send_message['To'] = email
    send_message['Subject'] = Header(subject, 'utf-8')

    message = MIMEText(message, 'html', 'utf-8')
    send_message.attach(message)

    context = ssl.create_default_context()

    with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_SSL_PORT, context=context) as server:
        server.login(sender, sender_password)
        server.sendmail(sender, email, send_message.as_string())

    return None
    
#################
def get_current_month_range() -> Tuple[date, date]:
    today = date.today()
    end_datetime = today
    start_datetime = (
        today.replace(day=1)
        if today.day > 1
        else (today - timedelta(days=1)).replace(day=1)
    )
    return start_datetime, end_datetime

def get_pre_month_range() -> Tuple[date, date]:
    end_date, _ = get_current_month_range()
    start_date = (end_date - timedelta(days=1)).replace(day=1)
    return start_date, end_date

def get_daily_range() -> Tuple[date, date]:
    end_date = date.today()
    start_date = end_date - timedelta(days=1)
    return start_date, end_date

def create_option(date_range: Tuple[date, date]) -> dict:
    return {
        "TimePeriod": {
            "Start": date_range[0].isoformat(),
            "End": date_range[1].isoformat(),
        },
        "Granularity": "MONTHLY",
        "Metrics": ["AmortizedCost"],
    }

def execute_get_cost(option: dict, ce) -> dict:
    resp = ce.get_cost_and_usage(**option)
    return {
        "start": resp["ResultsByTime"][0]["TimePeriod"]["Start"],
        "end": resp["ResultsByTime"][0]["TimePeriod"]["End"],
        "billing": resp["ResultsByTime"][0]["Total"]["AmortizedCost"]["Amount"],
        "unit": resp["ResultsByTime"][0]["Total"]["AmortizedCost"]["Unit"],
    }

def get_cost(ce, range: Tuple[date, date]):
    option = create_option(range)
    return execute_get_cost(option, ce)
    
def create_slack_message(data: dict, CUSTOMER: str, ACCOUNT: str) -> str:
    message_account = None
    message_premonth = None
    message_daily = None
    message_monthly = None

    premonth = None
    month = None
    daily = None
    account_name = f"{CUSTOMER}({ACCOUNT})"
    if account_name:
        message_account = f"계정명: {account_name}"
    if data["premonth"]["isSuccess"]:
        message_premonth = "지난 달: {0} {1} ({2}〜{3})".format(
            data["premonth"]["data"]["billing"],
            data["premonth"]["data"]["unit"],
            data["premonth"]["data"]["start"],
            data["premonth"]["data"]["end"],
        )
        premonth = Decimal(data["premonth"]["data"]["billing"])
    if data["monthly"]["isSuccess"]:
        message_monthly = "이번 달: {0} {1} ({2}〜{3})".format(
            data["monthly"]["data"]["billing"],
            data["monthly"]["data"]["unit"],
            data["monthly"]["data"]["start"],
            data["monthly"]["data"]["end"],
        )
        month = Decimal(data["monthly"]["data"]["billing"])
        if premonth:
            rate = int((month / premonth) * 10000) / 100
            message_monthly += f" (지난 달 대비: {rate}%)"
    if data["daily"]["isSuccess"]:
        message_daily = "어제: {0} {1} ({2}〜{3})".format(
            data["daily"]["data"]["billing"],
            data["daily"]["data"]["unit"],
            data["daily"]["data"]["start"],
            data["daily"]["data"]["end"],
        )
        daily = Decimal(data["daily"]["data"]["billing"])
        if month:
            rate = int((daily / month) * 10000) / 100
            message_daily += f" (이번 달 총비: {rate}%)"
        if premonth:
            rate = int((daily / premonth) * 10000) / 100
            message_daily += f" (지난 달 총비: {rate}%)"
    message = "\n".join(
        [
            x
            for x in ['<!channel>', message_account, message_monthly, message_premonth, message_daily]
            if x
        ]
    )
    return message

def create_email_message(data: dict, CUSTOMER: str, ACCOUNT: str) -> str:
    message_account = None
    message_premonth = None
    message_daily = None
    message_monthly = None

    premonth = None
    month = None
    daily = None
    account_name = f"{CUSTOMER}({ACCOUNT})"
    if account_name:
        message_account = f"계정명: {account_name}"
    if data["premonth"]["isSuccess"]:
        message_premonth = "지난 달: {0} {1} ({2}〜{3})".format(
            data["premonth"]["data"]["billing"],
            data["premonth"]["data"]["unit"],
            data["premonth"]["data"]["start"],
            data["premonth"]["data"]["end"],
        )
        premonth = Decimal(data["premonth"]["data"]["billing"])
    if data["monthly"]["isSuccess"]:
        message_monthly = "이번 달: {0} {1} ({2}〜{3})".format(
            data["monthly"]["data"]["billing"],
            data["monthly"]["data"]["unit"],
            data["monthly"]["data"]["start"],
            data["monthly"]["data"]["end"],
        )
        month = Decimal(data["monthly"]["data"]["billing"])
        if premonth:
            rate = int((month / premonth) * 10000) / 100
            message_monthly += f" (지난 달 대비: {rate}%)"
    if data["daily"]["isSuccess"]:
        message_daily = "어제: {0} {1} ({2}〜{3})".format(
            data["daily"]["data"]["billing"],
            data["daily"]["data"]["unit"],
            data["daily"]["data"]["start"],
            data["daily"]["data"]["end"],
        )
        daily = Decimal(data["daily"]["data"]["billing"])
        if month:
            rate = int((daily / month) * 10000) / 100
            message_daily += f" (이번 달 총비: {rate}%)"
        if premonth:
            rate = int((daily / premonth) * 10000) / 100
            message_daily += f" (지난 달 총비: {rate}%)"
    message = \
f"""
<html>
<head>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap');

        h1 {{
            font-family: "Gothic", sans-serif;
            font-size: 15pt;
        }}
        h3 {{
            font-family: "Gothic", sans-serif;
            font-size: 12pt;
        }}
        p {{
            font-family: "Gothic", sans-serif;
            font-size: 11pt;
            font-weight: normal;
        }}
        b {{
            font-family: "Gothic", sans-serif;
            font-size: 11pt;
        }}
    </style>
</head>
<body>
    <h1> AWS Account ID: {message_account} <h1>
    <hr />
    <p> {message_monthly} </p>
    <p> {message_premonth} </p>
    <p> {message_daily} </p>
</body>
</html>
"""
    return message

# run()

5. AWS EventBridge 이벤트 생성

규칙 생성시 규칙 유형은 일정(일정에 따라 실행되는 규칙)으로 설정합니다.
일정 패턴은 아래와 같이 입력하면 오전 10시 월요일 ~ 금요일에 매일 이벤트가 발생합니다.

Event 대상은 생성한 Lambda로 지정합니다.

0 1 ? * MON-FRI *

결과

1. 알림대상: 일반 유저(고객사)

  • webhook(slack)
  • email

2. 알림대상: 관리자

  • webhook(slack)
incoming-webhook
앱  오전 10:00
=============================
ABC Company |  AWS Secret Manager(123412341234) is match Spread Sheet(123412341234)
Notification Type: slack & email
----Slack MESSAGE----
@channel
계정명: ABC Company(123412341234)
이번 달: 209.9443508236 USD (2022-11-01〜2022-11-07) (지난 달 대비: 10.35%)
지난 달: 2028.2699061424 USD (2022-10-01〜2022-11-01)
어제: 13.8455826906 USD (2022-11-06〜2022-11-07) (이번 달 총비: 6.59%) (지난 달 총비: 0.68%)
----Email MESSAGE----
<html>
<head>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap');
        h1 {
            font-family: "Gothic", sans-serif;
            font-size: 15pt;
        }
        h3 {
            font-family: "Gothic", sans-serif;
            font-size: 12pt;
        }
        p {
            font-family: "Gothic", sans-serif;
            font-size: 11pt;
            font-weight: normal;
        }
        b {
            font-family: "Gothic", sans-serif;
            font-size: 11pt;
        }
    </style>
</head>
<body>
    <h1> AWS Account ID: 계정명: ABC Company(123412341234) <h1>
    <hr />
    <p> 이번 달: 209.9443508236 USD (2022-11-01〜2022-11-07) (지난 달 대비: 10.35%) </p>
    <p> 지난 달: 2028.2699061424 USD (2022-10-01〜2022-11-01) </p>
    <p> 어제: 13.8455826906 USD (2022-11-06〜2022-11-07) (이번 달 총비: 6.59%) (지난 달 총비: 0.68%) </p>
</body>
</html>
=============================
DEF Company(121212121212)  |  AWS Secret Manager(121212121212) is match Spread Sheet(121212121212)
INFO_(-----) --- 본부: 알림 비활성화
=============================
GHI Company(131313131313)  |  AWS Secret Manager(131313131313) is match Spread Sheet(131313131313)
INFO_(-----) --- 1: 알림 비활성화
=============================
JKL Company(141414141414)  |  AWS Secret Manager(141414141414) is match Spread Sheet(141414141414)
Notification Type: slack
----MESSAGE----
@channel
계정명: JKL Company(141414141414)
이번 달: 0.1441281548 USD (2022-11-01〜2022-11-07) (지난 달 대비: 7.42%)
지난 달: 1.9413167816 USD (2022-10-01〜2022-11-01)
어제: 0.00121 USD (2022-11-06〜2022-11-07) (이번 달 총비: 0.83%) (지난 달 총비: 0.06%)
=============================
MNO Company(151515151515)  |  AWS Secret Manager(151515151515) is match Spread Sheet(151515151515)
Notification Type: slack
----MESSAGE----
@channel
계정명: MNO Company(151515151515)
이번 달: 0.3240569314 USD (2022-11-01〜2022-11-07) (지난 달 대비: 0.32%)
지난 달: 100.475291374 USD (2022-10-01〜2022-11-01)
어제: 0.001665 USD (2022-11-06〜2022-11-07) (이번 달 총비: 0.51%) (지난 달 총비: 0.0%)
=============================
PQR Comapny(161616161616)  |  AWS Secret Manager(161616161616) is match Spread Sheet(161616161616)
Notification Type: slack
----MESSAGE----
@channel
계정명: PQR Comapny(161616161616)
이번 달: 1.0464451473 USD (2022-11-01〜2022-11-07) (지난 달 대비: 6.84%)
지난 달: 15.287026679 USD (2022-10-01〜2022-11-01)
어제: 0.169744304 USD (2022-11-06〜2022-11-07) (이번 달 총비: 16.22%) (지난 달 총비: 1.11%)
=============================
STU Company(212121212121)  |  AWS Secret Manager(212121212121) is match Spread Sheet(212121212121)
INFO_(고객사) ---- NFT: 알림 비활성화
=============================
VWXYZ Company(222222222222) |  AWS Secret Manager(222222222222) is match Spread Sheet(222222222222)
INFO_(고객사) ---- APP: 알림 비활성화