기본 구조 완성. 파일 정리하면 끝. 일단 배포 완료
todo - tf 파일의 slack_url 환경변수 등으로 정리 - 배포하는 스크립트? 필요할 것 같기도 함
This commit is contained in:
parent
91e986b475
commit
1b8b580554
6 changed files with 230 additions and 82 deletions
53
main.py
53
main.py
|
|
@ -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
155
main.tf
|
|
@ -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
39
menu.py
|
|
@ -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
14
requirements.txt
Normal 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
|
||||
39
schedule.py
39
schedule.py
|
|
@ -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']
|
||||
|
||||
|
|
|
|||
12
slack.py
12
slack.py
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue