From 1b8b5805541713911fb3c8b060b87ec2749e649c Mon Sep 17 00:00:00 2001 From: bumpsoo Date: Wed, 3 Jul 2024 05:06:54 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=99=84=EC=84=B1.=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=A9=B4=20=EB=81=9D.=20=EC=9D=BC=EB=8B=A8=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit todo - tf 파일의 slack_url 환경변수 등으로 정리 - 배포하는 스크립트? 필요할 것 같기도 함 --- main.py | 53 +++++++--------- main.tf | 155 ++++++++++++++++++++++++++++++++++++++--------- menu.py | 39 ++++++++++-- requirements.txt | 14 +++++ schedule.py | 39 ++++++------ slack.py | 12 ++++ 6 files changed, 230 insertions(+), 82 deletions(-) create mode 100644 requirements.txt diff --git a/main.py b/main.py index 503eed6..6a0333b 100644 --- a/main.py +++ b/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)) + diff --git a/main.tf b/main.tf index 234d4d3..4015482 100644 --- a/main.tf +++ b/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 = < 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 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..421991f --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/schedule.py b/schedule.py index 59c5e1c..10ad67b 100644 --- a/schedule.py +++ b/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'] diff --git a/slack.py b/slack.py index 925e37c..57a5083 100644 --- a/slack.py +++ b/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' +