기본 구조 완성. 파일 정리하면 끝. 일단 배포 완료

todo
- tf 파일의 slack_url 환경변수 등으로 정리
- 배포하는 스크립트? 필요할 것 같기도 함
This commit is contained in:
bumpsoo 2024-07-03 05:06:54 +09:00
parent 91e986b475
commit 1b8b580554
6 changed files with 230 additions and 82 deletions

53
main.py
View file

@ -1,13 +1,12 @@
from datetime import datetime from datetime import datetime
import json from typing import Dict, Any, Final, Optional
from typing import Dict, Any, Final, List, Optional
import requests
import os import os
import time import time
from dataclasses import dataclass from dataclasses import dataclass
import menu import menu
import schedule import schedule
import slack
# Load environment variables (recommended for sensitive data) # Load environment variables (recommended for sensitive data)
@ -37,43 +36,35 @@ def __parse_param(evt: Dict[str, Any]) -> Optional[Param]:
return Param(str(evt.get('slack_url')), date, cnt + 1) return Param(str(evt.get('slack_url')), date, cnt + 1)
def retry(p: Param, lambda_arn: str, schedule_role_arn: str) -> Dict[str, str]: def retry(p: Param, lambda_arn: str, schedule_role_arn: str) -> Dict[str, str]:
schedule.one_time_schedule(lambda_arn, schedule_role_arn, RETRY_INTERVAL, p) schedule.one_time_schedule(lambda_arn, schedule_role_arn, RETRY_INTERVAL, p.slack_url, p.date, p.count)
return {'statusCode': '200', 'body': f'retry {p.count}'} return {'statusCode': '200', 'body': f'retry {p.count}'}
def wrap_return(ret: bool) -> Dict[str, str]:
return {
'statusCode': '200',
'body': 'success' if ret else 'fail'
}
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]: def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]:
os.environ['TZ'] = 'Asia/Seoul' os.environ['TZ'] = 'Asia/Seoul'
time.tzset() time.tzset()
lambda_arn = os.environ['LAMBDA_ARN'] if context.invoked_function_arn is None:
raise Exception('None???')
lambda_arn = str(context.invoked_function_arn)
schedule_role_arn = os.environ['SCHEDULE_ROLE_ARN'] schedule_role_arn = os.environ['SCHEDULE_ROLE_ARN']
bucket_name = os.environ['S3_BUCKET_NAME']
param = __parse_param(event) param = __parse_param(event)
if param is None: if param is None:
return PARAM_ERROR return PARAM_ERROR
menus = menu.menu(param.date) menus = menu.menu(param.date)
menus_without_img = [ x for x in menus if x.menu_image is None ] menus_without_img = [ x for x in menus if x.menu_image is None ]
if param.count < MAX_RETRY and (len(menus) == 0 or len(menus_without_img) > 0): print('menus', menus)
print('menus_without_img', menus_without_img)
print('param', param)
if param.count <= MAX_RETRY and (len(menus) == 0 or len(menus_without_img) > 0):
return retry(param, lambda_arn, schedule_role_arn) return retry(param, lambda_arn, schedule_role_arn)
if len(menus) == 0:
# 슬랙 메시지 전송( 오늘의 메뉴 존재 X ) for each in menus:
return retry(param, lambda_arn, schedule_role_arn) each.resize_image(bucket_name, param.date)
# Extract relevant data for your Slack message (adjust as needed) return wrap_return(slack.send(param.slack_url, param.date, menus))
message_content: str = f"*Important Data from External API*\n"
message_content += f"`\n{json.dumps(api_data, indent=4)}\n`"
# Construct payload for the Slack webhook
slack_payload: Dict[str, Any] = {
"text": message_content,
"blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": message_content}}
]
}
# Send the message to Slack
slack_response: requests.Response = requests.post(
'',
#SLACK_WEBHOOK_URL,
data=json.dumps(slack_payload),
headers={'Content-Type': 'application/json'}
)
slack_response.raise_for_status()
return {
'statusCode': '200',
'body': json.dumps('Data successfully sent to Slack.')
}

155
main.tf
View file

@ -10,30 +10,50 @@ locals {
lambda_role_name = "${local.prefix}-lambda-role" lambda_role_name = "${local.prefix}-lambda-role"
lambda_function_name = "${local.prefix}-lambda" lambda_function_name = "${local.prefix}-lambda"
lambda_filename = "artifacts.zip" # Zip file containing Lambda code lambda_filename = "artifacts.zip" # Zip file containing Lambda code
lambda_handler = "lambda_function.lambda_handler" # Replace with your handler lambda_handler = "main.lambda_handler" # Replace with your handler
weekday_rule_name = "${local.prefix}-weekday-image-upload" weekday_rule_name = "${local.prefix}-weekday-image-upload"
schedule_role_name = "${local.prefix}-schedule-role"
} }
# S3 Bucket (Publicly Accessible) # S3 Bucket (Publicly Accessible)
resource "aws_s3_bucket" "image_bucket" { resource "aws_s3_bucket" "image_bucket" {
bucket = local.image_bucket_name bucket = local.image_bucket_name
acl = "public-read"
# Policy for public read access to objects
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "arn:aws:s3:::${aws_s3_bucket.image_bucket.bucket}/*"
}
]
})
} }
# S3 Bucket Public Access Block (disabled for objects)
resource "aws_s3_bucket_public_access_block" "image_bucket_public_access_block" {
bucket = aws_s3_bucket.image_bucket.id
block_public_acls = false # Block public ACLs
block_public_policy = false # Block public bucket policies
ignore_public_acls = false # Ignore public ACLs on existing objects
restrict_public_buckets = false # Restrict public bucket policies on existing buckets
}
resource "aws_s3_bucket_policy" "image_bucket_policy" {
depends_on = [
aws_s3_bucket.image_bucket,
aws_s3_bucket_public_access_block.image_bucket_public_access_block
]
bucket = aws_s3_bucket.image_bucket.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bumpsoo-menu-img-bucket/*"
}
]
}
EOF
}
# IAM Role for Lambda (EventBridge Permissions) # IAM Role for Lambda (EventBridge Permissions)
resource "aws_iam_role" "lambda_role" { resource "aws_iam_role" "lambda_role" {
name = local.lambda_role_name name = local.lambda_role_name
@ -60,8 +80,29 @@ resource "aws_iam_role" "lambda_role" {
{ {
Effect = "Allow" Effect = "Allow"
Action = [ Action = [
"events:PutRule", "scheduler:CreateSchedule",
"events:PutTargets" "scheduler:UpdateSchedule",
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"iam:PassRole",
]
Resource = "*"
},
{
Effect = "Allow"
Action = "s3:PutObject"
Resource = "*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:PutLogEvents"
] ]
Resource = "*" Resource = "*"
} }
@ -70,6 +111,45 @@ resource "aws_iam_role" "lambda_role" {
} }
} }
# IAM Role for Lambda (EventBridge Permissions)
resource "aws_iam_role" "schedule_role" {
name = local.schedule_role_name
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Principal = {
Service = "scheduler.amazonaws.com"
}
Effect = "Allow"
}
]
})
# Policy to allow EventBridge rule creation/management
inline_policy {
name = "schedule_invoke_any_lambda"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"lambda:InvokeFunction"
]
Resource = "*"
}
]
})
}
}
resource "aws_cloudwatch_log_group" "image_lambda_log_group" {
name = "/aws/lambda/${local.lambda_function_name}"
retention_in_days = 14 # Adjust retention period as needed
}
# Lambda Function # Lambda Function
resource "aws_lambda_function" "image_lambda" { resource "aws_lambda_function" "image_lambda" {
function_name = local.lambda_function_name function_name = local.lambda_function_name
@ -77,19 +157,34 @@ resource "aws_lambda_function" "image_lambda" {
role = aws_iam_role.lambda_role.arn role = aws_iam_role.lambda_role.arn
handler = local.lambda_handler handler = local.lambda_handler
runtime = "python3.11" runtime = "python3.11"
timeout = 60
environment {
variables = {
SCHEDULE_ROLE_ARN = aws_iam_role.schedule_role.arn
S3_BUCKET_NAME = local.image_bucket_name
}
}
depends_on = [aws_cloudwatch_log_group.image_lambda_log_group]
} }
# EventBridge Rule
resource "aws_cloudwatch_event_rule" "weekday_rule" { resource "aws_scheduler_schedule" "weekday_schedule" {
name = local.weekday_rule_name name = local.weekday_rule_name # Keep the same name
description = "Trigger Lambda at 10 AM on weekdays" description = "Trigger Lambda at 10 AM on weekdays (KST)"
schedule_expression = "cron(0 10 ? * MON-FRI *)" # 10 AM every workday in KST timezone schedule_expression = "cron(30 10 ? * MON-FRI *)"
} schedule_expression_timezone = "Asia/Seoul"
flexible_time_window {
# EventBridge Target (Lambda) mode = "OFF"
resource "aws_cloudwatch_event_target" "lambda_target" { }
rule = aws_cloudwatch_event_rule.weekday_rule.name
target_id = "lambda" target {
arn = aws_lambda_function.image_lambda.arn arn = aws_lambda_function.image_lambda.arn
role_arn = aws_iam_role.schedule_role.arn
input = <<EOF
{"slack_url": "https://hooks.slack.com/services/"}
EOF
}
} }

39
menu.py
View file

@ -1,9 +1,13 @@
# menu repository # menu repository
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, Final, List, Optional from datetime import datetime
from typing import Dict, Final, List, Optional
import requests import requests
from PIL import Image
from io import BytesIO
import boto3
import slack
BASE: Final[str] = 'https://sfmn.shinsegaefood.com/' BASE: Final[str] = 'https://sfmn.shinsegaefood.com/'
URL: Final[str] = f'{BASE}selectTodayMenu2.do' URL: Final[str] = f'{BASE}selectTodayMenu2.do'
@ -26,6 +30,31 @@ class Menu:
def __post_init__(self): def __post_init__(self):
if self.menu_image: if self.menu_image:
self.menu_image = f"https://store.shinsegaefood.com/{self.menu_image}" self.menu_image = f"https://store.shinsegaefood.com/{self.menu_image}"
def resize_image(self, bucket_name: str, date: str):
if self.menu_image is None:
return
image_url = self.menu_image
filename: str = date + '/' + str(datetime.now().timestamp()) + 'png'
response = requests.get(image_url)
response.raise_for_status()
target_size = 1024 * 1024
image_data = response.content
img = Image.open(BytesIO(image_data))
while len(image_data) > target_size:
resize_factor = 0.9
new_width, new_height = int(img.width * resize_factor), int(img.height * resize_factor)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
img_bytes = BytesIO()
img.save(img_bytes, format='png')
image_data = img_bytes.getvalue()
s3 = boto3.client("s3")
s3.put_object(
Body=image_data,
Bucket=bucket_name,
Key=filename,
)
self.menu_image = f"https://{bucket_name}.s3.amazonaws.com/{filename}"
# date should be this form 20240601 # date should be this form 20240601
def menu(date: str) -> List[Menu]: def menu(date: str) -> List[Menu]:
@ -42,6 +71,7 @@ def menu(date: str) -> List[Menu]:
data: List[Dict[str, str]] = [] data: List[Dict[str, str]] = []
try: try:
data = res.json()['model']['model'] data = res.json()['model']['model']
print('data', data)
except: except:
return menus return menus
for each in data: for each in data:
@ -49,11 +79,12 @@ def menu(date: str) -> List[Menu]:
menus.append(Menu( menus.append(Menu(
each['MEAL_TYPE_NM'], each['MEAL_TYPE_NM'],
each['DINNER_TYPE_NM'], each['DINNER_TYPE_NM'],
each['REP_TYPE_NM'], each['REP_MENU_NM'],
each['MENU_DESC'], each['MENU_DESC'],
each['TOT_CALORY'], each['TOT_CALORY'],
each.get('MEAL_TYPE_NM'), each.get('WEB_LINK'),
)) ))
except: except:
pass pass
return menus return menus

14
requirements.txt Normal file
View file

@ -0,0 +1,14 @@
boto3==1.34.136
botocore==1.34.136
certifi==2024.6.2
charset-normalizer==3.3.2
idna==3.7
jmespath==1.0.1
nodeenv==1.9.1
pillow==10.4.0
pyright==1.1.369
python-dateutil==2.9.0.post0
requests==2.32.3
s3transfer==0.10.2
six==1.16.0
urllib3==2.2.2

View file

@ -1,38 +1,43 @@
import boto3 import boto3
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json import json
import main
def one_time_schedule( def one_time_schedule(
lambda_arn: str, lambda_arn: str,
schedule_role_arn: str, schedule_role_arn: str,
minutes: int, minutes: int,
param: main.Param slack_url: str,
date: str,
count: int,
) -> str: ) -> str:
client = boto3.client('scheduler') client = boto3.client('scheduler')
schedule_time = datetime.now() + timedelta(minutes = minutes) schedule_time = datetime.now() + timedelta(minutes = minutes)
schedule_expression = f"at({schedule_time.isoformat()})" schedule_expression = f"at({schedule_time.strftime('%Y-%m-%dT%H:%M:%S')})"
schedule_name = f"menu-trigger-{schedule_time.strftime('%Y%m%d%H%M%S')}" schedule_name = f"menu-trigger-{schedule_time.strftime('%Y%m%d%H%M%S')}"
target = {
'Arn': lambda_arn,
'RoleArn': schedule_role_arn,
'Input': json.dumps({
'slack_url': slack_url,
'date': date,
'count': count
}),
'RetryPolicy': {'MaximumRetryAttempts': 0}
}
rule_response = client.create_schedule( rule_response = client.create_schedule(
Name = schedule_name, Name = schedule_name,
ScheduleExpression = schedule_expression, ScheduleExpression = schedule_expression,
ScheduleExpressionTimezone = 'Asia/Seoul', ScheduleExpressionTimezone = 'Asia/Seoul',
State = 'ENABLED', State = 'ENABLED',
Target = { FlexibleTimeWindow = {'Mode': 'OFF'},
'Arn': lambda_arn, Target = target
'RoleArn': schedule_role_arn,
'Input': json.dumps({
'slack_url': param.slack_url,
'date': param.date,
'count': param.count + 1
}),
'RetryPolicy': {'MaximumRetryAttempts': 0}
},
) )
client.update_schedule( rule_response = client.update_schedule(
Name = schedule_name, Name = schedule_name,
ActionAfterCompletion = 'DELETE' ScheduleExpression = schedule_expression,
FlexibleTimeWindow = {'Mode': 'OFF'},
ActionAfterCompletion = 'DELETE',
Target = target
) )
return rule_response['RuleArn'] return rule_response['ScheduleArn']

View file

@ -1,4 +1,6 @@
from typing import Any, Dict, List from typing import Any, Dict, List
import requests
from menu import Menu from menu import Menu
def markdown(text: str) -> Dict[str, Any]: def markdown(text: str) -> Dict[str, Any]:
@ -33,6 +35,9 @@ def menu_to_str(m: Menu) -> str:
def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]: def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
blocks = [markdown(f'*{date} 일자 메뉴*')] blocks = [markdown(f'*{date} 일자 메뉴*')]
if len(data) == 0:
blocks.append(markdown('메뉴가 존재하지 않습니다'))
return {'blocks': blocks}
for each in data: for each in data:
blocks.append(markdown(menu_to_str(each))) blocks.append(markdown(menu_to_str(each)))
if each.menu_image: if each.menu_image:
@ -41,3 +46,10 @@ def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
blocks.append(markdown('이미지가 존재하지 않습니다')) blocks.append(markdown('이미지가 존재하지 않습니다'))
return {'blocks': blocks} return {'blocks': blocks}
def send(slack_url: str, date: str, data: List[Menu]) -> bool:
payload = for_slack(date, data)
print(payload)
res = requests.post(url= slack_url, json=payload)
print(res.text)
return res.text == 'ok'