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

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
import json
from typing import Dict, Any, Final, List, Optional
import requests
from typing import Dict, Any, Final, Optional
import os
import time
from dataclasses import dataclass
import menu
import schedule
import slack
# 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)
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}'}
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]:
os.environ['TZ'] = 'Asia/Seoul'
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']
bucket_name = os.environ['S3_BUCKET_NAME']
param = __parse_param(event)
if param is None:
return PARAM_ERROR
menus = menu.menu(param.date)
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)
if len(menus) == 0:
# 슬랙 메시지 전송( 오늘의 메뉴 존재 X )
return retry(param, lambda_arn, schedule_role_arn)
# Extract relevant data for your Slack message (adjust as needed)
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.')
}
for each in menus:
each.resize_image(bucket_name, param.date)
return wrap_return(slack.send(param.slack_url, param.date, menus))

155
main.tf
View file

@ -10,30 +10,50 @@ locals {
lambda_role_name = "${local.prefix}-lambda-role"
lambda_function_name = "${local.prefix}-lambda"
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"
schedule_role_name = "${local.prefix}-schedule-role"
}
# S3 Bucket (Publicly Accessible)
resource "aws_s3_bucket" "image_bucket" {
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)
resource "aws_iam_role" "lambda_role" {
name = local.lambda_role_name
@ -60,16 +80,76 @@ resource "aws_iam_role" "lambda_role" {
{
Effect = "Allow"
Action = [
"events:PutRule",
"events:PutTargets"
"scheduler:CreateSchedule",
"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 = "*"
}
]
})
}
}
# 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
resource "aws_lambda_function" "image_lambda" {
function_name = local.lambda_function_name
@ -77,19 +157,34 @@ resource "aws_lambda_function" "image_lambda" {
role = aws_iam_role.lambda_role.arn
handler = local.lambda_handler
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" {
name = local.weekday_rule_name
description = "Trigger Lambda at 10 AM on weekdays"
schedule_expression = "cron(0 10 ? * MON-FRI *)" # 10 AM every workday in KST timezone
}
# EventBridge Target (Lambda)
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.weekday_rule.name
target_id = "lambda"
arn = aws_lambda_function.image_lambda.arn
resource "aws_scheduler_schedule" "weekday_schedule" {
name = local.weekday_rule_name # Keep the same name
description = "Trigger Lambda at 10 AM on weekdays (KST)"
schedule_expression = "cron(30 10 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Seoul"
flexible_time_window {
mode = "OFF"
}
target {
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
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
from PIL import Image
from io import BytesIO
import boto3
import slack
BASE: Final[str] = 'https://sfmn.shinsegaefood.com/'
URL: Final[str] = f'{BASE}selectTodayMenu2.do'
@ -26,6 +30,31 @@ class Menu:
def __post_init__(self):
if 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
def menu(date: str) -> List[Menu]:
@ -42,6 +71,7 @@ def menu(date: str) -> List[Menu]:
data: List[Dict[str, str]] = []
try:
data = res.json()['model']['model']
print('data', data)
except:
return menus
for each in data:
@ -49,11 +79,12 @@ def menu(date: str) -> List[Menu]:
menus.append(Menu(
each['MEAL_TYPE_NM'],
each['DINNER_TYPE_NM'],
each['REP_TYPE_NM'],
each['REP_MENU_NM'],
each['MENU_DESC'],
each['TOT_CALORY'],
each.get('MEAL_TYPE_NM'),
each.get('WEB_LINK'),
))
except:
pass
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
from datetime import datetime, timedelta
import json
import main
def one_time_schedule(
lambda_arn: str,
schedule_role_arn: str,
minutes: int,
param: main.Param
slack_url: str,
date: str,
count: int,
) -> str:
client = boto3.client('scheduler')
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')}"
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(
Name = schedule_name,
ScheduleExpression = schedule_expression,
ScheduleExpressionTimezone = 'Asia/Seoul',
State = 'ENABLED',
Target = {
'Arn': lambda_arn,
'RoleArn': schedule_role_arn,
'Input': json.dumps({
'slack_url': param.slack_url,
'date': param.date,
'count': param.count + 1
}),
'RetryPolicy': {'MaximumRetryAttempts': 0}
},
FlexibleTimeWindow = {'Mode': 'OFF'},
Target = target
)
client.update_schedule(
rule_response = client.update_schedule(
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
import requests
from menu import Menu
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]:
blocks = [markdown(f'*{date} 일자 메뉴*')]
if len(data) == 0:
blocks.append(markdown('메뉴가 존재하지 않습니다'))
return {'blocks': blocks}
for each in data:
blocks.append(markdown(menu_to_str(each)))
if each.menu_image:
@ -41,3 +46,10 @@ def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
blocks.append(markdown('이미지가 존재하지 않습니다'))
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'