diff --git a/src/campaign_service/confirm_attendance/.dockerignore b/src/campaign_service/confirm_attendance/.dockerignore new file mode 100644 index 0000000..e0bde70 --- /dev/null +++ b/src/campaign_service/confirm_attendance/.dockerignore @@ -0,0 +1,4 @@ +.env +samconfig.toml +template.yaml +.aws-sam diff --git a/src/campaign_service/confirm_attendance/Dockerfile b/src/campaign_service/confirm_attendance/Dockerfile new file mode 100644 index 0000000..6d4f6c1 --- /dev/null +++ b/src/campaign_service/confirm_attendance/Dockerfile @@ -0,0 +1,11 @@ +FROM public.ecr.aws/lambda/python:3.11 + +# Install dependencies +COPY requirements.txt /var/task/ +RUN pip install -r /var/task/requirements.txt + +# Copy function code +COPY . /var/task/ + +# Set the command to run the Lambda function +CMD ["lambda_function.lambda_handler"] diff --git a/src/campaign_service/confirm_attendance/dynamodb.py b/src/campaign_service/confirm_attendance/dynamodb.py new file mode 100644 index 0000000..08a3995 --- /dev/null +++ b/src/campaign_service/confirm_attendance/dynamodb.py @@ -0,0 +1,24 @@ +import logging + +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +class DynamoDB: + """ + DynamoDB operations + """ + def __init__(self): + self.dynamodb_client = boto3.resource( + 'dynamodb' + ) + + def put_item(self, table_name: str, item: dict): + try: + table = self.dynamodb_client.Table(table_name) + table.put_item(Item=item) + logger.info("Saved email record to DynamoDB: %s", item.get("email_id")) + except Exception as e: + logger.error("Error saving email record to DynamoDB: %s", e) + raise \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/lambda_function.py b/src/campaign_service/confirm_attendance/lambda_function.py new file mode 100644 index 0000000..280779b --- /dev/null +++ b/src/campaign_service/confirm_attendance/lambda_function.py @@ -0,0 +1,123 @@ +import json +import logging + +from dynamodb import DynamoDB +from ses import SES + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def lambda_handler(event, context): + try: + code = event['queryStringParameters']['code'] + is_attend = event['queryStringParameters']['is_attend'] + logger.info(f"Received code: {code}, is_attend: {is_attend}") + + # * 先寫死 + # TODO 之後應該要換成從s3讀名單, 想辦法生成unique code + dummy_participants = [ + {'code': '1', 'name': 'Richie', 'email': 'rich.liu627@gmail.com'}, + {'code': '2', 'name': 'Shiun', 'email': 'shiunchiu.me@gmail.com'}, + {'code': '3', 'name': 'Harry', 'email': 'harryup2000@gmail.com'} + ] + + participant = next((p for p in dummy_participants if p['code'] == code), None) + logger.info("Participant: %s", participant) + + if participant: + # * 這邊就把display_name寫死 + send_confirmation_email("no-reply", participant['name'], participant['email'], is_attend) + + save_to_dynamodb(participant['name'], 'c14274978654c2488923d7fee1eb61f', participant['code'], participant['email'], is_attend) + + html_response = f""" + +
+ + + +恭喜您成功登記出席,稍後會收到一封系統信件以表示系統已收到回應。
+若沒有收到該信件,務必聯繫我們:
+Facebook: 台灣 AWS Educate Cloud Ambassador Facebook
+Instagram: 台灣 AWS Educate Cloud Ambassador Instagram
+ + + """ + + response = { + 'statusCode': 200, + 'body': html_response, + 'headers': { + 'Content-Type': 'text/html' + } + } + + logger.info("Response: %s", response) + return response + + except Exception as e: + logger.error("Error in lambda_handler: %s", e, exc_info=True) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'status': "error", + 'message': f"Internal server error: {str(e)}" + }), + 'headers': { + 'Content-Type': 'application/json' + } + } + +def send_confirmation_email(display_name, name, email, is_attend): + try: + ses = SES() + source_email = "awseducate.cloudambassador@gmail.com" + formatted_source_email = f"{display_name} <{source_email}>" + email_title = "出席確認" + + # 根據 is_attend 的值設定出席狀態文本 + attendance_status = "會出席" if is_attend == "true" else "不會出席" + + formatted_content = f""" + + + + + +{name} 你好,
+您的出席狀態已更新為 {attendance_status}。感謝您的回應!
+若您未收到確認郵件,請聯繫我們:
+Facebook: 台灣 AWS Educate Cloud Ambassador Facebook
+Instagram: 台灣 AWS Educate Cloud Ambassador Instagram
+ + + """ + ses.send_email( + formatted_source_email=formatted_source_email, + receiver_email=email, + email_title=email_title, + formatted_content=formatted_content + ) + logger.info("Confirmation email sent to %s", email) + + except Exception as e: + logger.error("Failed to send confirmation email to %s: %s", email, e) + raise + +def save_to_dynamodb(name, campaign_id, participant_id, email, is_attend): + try: + dynamodb = DynamoDB() + item = { + 'campaign_id': campaign_id, + 'participant_id': participant_id, + 'name': name, + 'email': email, + 'is_attend': is_attend + } + dynamodb.put_item('campaign', item) + logger.info("Record saved to DynamoDB: %s", item) + + except Exception as e: + logger.error("Failed to save record to DynamoDB: %s", e) + raise \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/requirements.txt b/src/campaign_service/confirm_attendance/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/campaign_service/confirm_attendance/s3.py b/src/campaign_service/confirm_attendance/s3.py new file mode 100644 index 0000000..1bd290e --- /dev/null +++ b/src/campaign_service/confirm_attendance/s3.py @@ -0,0 +1,76 @@ +import logging + +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +class S3: + """ + S3 operations + """ + def __init__(self): + self.s3_client = boto3.client( + 's3' + ) + + def create_bucket(self, bucket_name: str): + """ + Create an S3 bucket + """ + try: + self.s3_client.create_bucket( + Bucket=bucket_name + ) + logger.info("Bucket %s created successfully.", bucket_name) + except Exception as e: + logger.error("Error creating bucket %s: %s", bucket_name, e) + + def upload_file(self, file_name: str, bucket_name: str, object_name: str = None): + """ + Upload a file to an S3 bucket + """ + if object_name is None: + object_name = file_name + + try: + self.s3_client.upload_file(file_name, bucket_name, object_name) + logger.info("File %s uploaded to %s/%s.", file_name, bucket_name, object_name) + except Exception as e: + logger.error("Error uploading file %s to bucket %s: %s", file_name, bucket_name, e) + + def list_files(self, bucket_name: str): + """ + List files in an S3 bucket + """ + try: + response = self.s3_client.list_objects_v2(Bucket=bucket_name) + if 'Contents' in response: + for obj in response['Contents']: + logger.info("File found: %s", obj['Key']) + else: + logger.info("No files found in bucket %s.", bucket_name) + except Exception as e: + logger.error("Error listing files in bucket %s: %s", bucket_name, e) + + def download_file(self, bucket_name: str, file_key: str, local_file_path: str): + """ + Download a single file from an S3 bucket to a local path + """ + try: + self.s3_client.download_file(bucket_name, file_key, local_file_path) + logger.info("Downloaded file %s from bucket %s to %s.", file_key, bucket_name, local_file_path) + except Exception as e: + logger.error("Error downloading file %s from bucket %s: %s", file_key, bucket_name, e) + + def get_object(self, bucket_name: str, key: str): + """ + Get object from an S3 bucket + """ + try: + response = self.s3_client.get_object(Bucket=bucket_name, Key=key) + return response + except Exception as e: + logger.error("Error getting object %s from bucket %s: %s", key, bucket_name, e) + raise \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/samconfig.toml b/src/campaign_service/confirm_attendance/samconfig.toml new file mode 100644 index 0000000..94c2e9b --- /dev/null +++ b/src/campaign_service/confirm_attendance/samconfig.toml @@ -0,0 +1,9 @@ +version = 0.1 +[default.deploy.parameters] +stack_name = "confirm-attendence" +resolve_s3 = true +s3_prefix = "confirm-attendence" +region = "us-east-1" +capabilities = "CAPABILITY_NAMED_IAM" +parameter_overrides = "AwsRegion=\"us-east-1\"" +image_repositories = ["CampaignFunction=070576557102.dkr.ecr.us-east-1.amazonaws.com/confirmattendence01cecb19/campaignfunction708cd7aerepo"] diff --git a/src/campaign_service/confirm_attendance/ses.py b/src/campaign_service/confirm_attendance/ses.py new file mode 100644 index 0000000..9a93dd9 --- /dev/null +++ b/src/campaign_service/confirm_attendance/ses.py @@ -0,0 +1,33 @@ +import logging + +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +class SES: + """ + SES operations + """ + def __init__(self): + self.ses_client = boto3.client( + 'ses', + region_name='ap-northeast-1' + ) + + def send_email(self, formatted_source_email: str, receiver_email: str, email_title: str, formatted_content: str): + try: + response = self.ses_client.send_email( + Source=formatted_source_email, + Destination={ + "ToAddresses": [receiver_email]}, + Message={ + "Subject": {"Data": email_title}, + "Body": {"Html": {"Data": formatted_content}}, + }, + ) + return response + except Exception as e: + logger.error("Error sending email to %s: %s", receiver_email, e) + raise \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/sqs.py b/src/campaign_service/confirm_attendance/sqs.py new file mode 100644 index 0000000..57b3ece --- /dev/null +++ b/src/campaign_service/confirm_attendance/sqs.py @@ -0,0 +1,26 @@ +import boto3 +import logging + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +class SQS: + """ + SQS operations + """ + def __init__(self): + self.sqs_client = boto3.client( + 'sqs' + ) + + def delete_message(self, queue_url: str, receipt_handle: str): + try: + self.sqs_client.delete_message( + QueueUrl=queue_url, + ReceiptHandle=receipt_handle + ) + logger.info("Deleted message from SQS: %s", receipt_handle) + except Exception as e: + logger.error("Error deleting message from SQS: %s", e) + raise \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/template.yaml b/src/campaign_service/confirm_attendance/template.yaml new file mode 100644 index 0000000..cffd585 --- /dev/null +++ b/src/campaign_service/confirm_attendance/template.yaml @@ -0,0 +1,116 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: AWS SAM template for attendance confirmation + +Parameters: + AwsRegion: + Type: String + Default: "us-east-1" + Description: AWS region + +Resources: + CampaignFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: confirm-attendance + Role: !GetAtt LambdaExecutionRole.Arn + PackageType: Image + Environment: + Variables: + TABLE_NAME: !Ref CampaignTable + BUCKET_NAME: local-dev-aws-educate-tpet-storage + SQS_QUEUE_URL: !Sub "https://sqs.${AwsRegion}.amazonaws.com/070576557102/email-queue" + Timeout: 10 + Events: + CampaignApi: + Type: Api + Properties: + Path: /attendances + Method: get + RestApiId: !Ref CampaignApi + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.11 + + CampaignApi: + Type: AWS::Serverless::Api + Properties: + Name: CampaignAPI + StageName: dev + EndpointConfiguration: REGIONAL + + LambdaExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + RoleName: !Sub "LambdaExecutionRole-${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" + Policies: + - PolicyName: !Sub "LambdaPolicy-${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "arn:aws:logs:*:*:*" + - Effect: Allow + Action: + - s3:PutObject + - s3:GetObject + - s3:DeleteObject + Resource: "arn:aws:s3:::email-sender-excel/*" + - Effect: Allow + Action: + - s3:ListBucket + Resource: "arn:aws:s3:::email-sender-excel" + - Effect: Allow + Action: + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:GetItem + Resource: !Sub "arn:aws:dynamodb:${AwsRegion}:${AWS::AccountId}:table/${CampaignTable}" + - Effect: Allow + Action: + - sqs:SendMessage + Resource: !Sub "arn:aws:sqs:${AwsRegion}:070576557102:email-queue" + - Effect: Allow + Action: + - ses:SendEmail + - ses:SendRawEmail + Resource: "*" + + CampaignTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: campaign + AttributeDefinitions: + - AttributeName: campaign_id + AttributeType: S + - AttributeName: participant_id + AttributeType: S + + KeySchema: + - AttributeName: campaign_id + KeyType: HASH + - AttributeName: participant_id + KeyType: RANGE + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + +Outputs: + CampaignApiUrl: + Description: URL of the API Gateway for campaign function + Value: !Sub "https://${CampaignApi}.execute-api.${AwsRegion}.amazonaws.com/dev/attendances" + SQSQueueUrl: + Description: URL of the SQS Queue + Value: !Sub "arn:aws:sqs:${AwsRegion}:070576557102:email-queue" \ No newline at end of file diff --git a/src/campaign_service/confirm_attendance/utils.py b/src/campaign_service/confirm_attendance/utils.py new file mode 100644 index 0000000..256602d --- /dev/null +++ b/src/campaign_service/confirm_attendance/utils.py @@ -0,0 +1,153 @@ +import datetime +import io +import logging +import os +import re +import uuid + +import pandas as pd +import requests +from botocore.exceptions import ClientError +from dynamodb import DynamoDB +from s3 import S3 +from ses import SES + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +BUCKET_NAME = "aws_educate_tpet_storage" + + +def get_file_info(file_id): + try: + api_url = f"https://api.tpet.aws-educate.tw/dev/files/{file_id}" + response = requests.get(api_url) + response.raise_for_status() + logger.info("Fetched file info for file_id: %s", file_id) + return response.json() + except Exception as e: + logger.error("Error in get_file_info: %s", e) + raise + + +def get_template(template_file_s3_key): + try: + s3 = S3() + request = s3.get_object(BUCKET_NAME, template_file_s3_key) + template_content = request["Body"].read().decode("utf-8") + logger.info("Fetched template content from S3 key: %s", template_file_s3_key) + return template_content + except Exception as e: + logger.error("Error in get_template: %s", e) + raise + + +def read_sheet_data_from_s3(spreadsheet_file_s3_key): + try: + s3 = S3() + request = s3.get_object(BUCKET_NAME, spreadsheet_file_s3_key) + xlsx_content = request["Body"].read() + excel_data = pd.read_excel(io.BytesIO(xlsx_content), engine="openpyxl") + rows = excel_data.to_dict(orient="records") + if excel_data.empty: + return [], 0 + logger.info("Read sheet data from S3 key: %s", spreadsheet_file_s3_key) + return rows, excel_data.columns.tolist() + except Exception as e: + logger.error("Error in read excel from s3: %s", e) + raise + + +def send_email(email_title, template_content, row, display_name): + try: + ses = SES() + template_content = template_content.replace("\r", "") + template_content = re.sub(r"\{\{(.*?)\}\}", r"{\1}", template_content) + receiver_email = row.get("Email") + if not receiver_email: + logger.warning("Email address not found in row: %s", row) + return None, "FAILED" + try: + formatted_row = {k: str(v) for k, v in row.items()} + formatted_content = template_content.format(**formatted_row) + source_email = "awseducate.cloudambassador@gmail.com" + formatted_source_email = f"{display_name} <{source_email}>" + ses.send_email( + formatted_source_email=formatted_source_email, + receiver_email=receiver_email, + email_title=email_title, + formatted_content=formatted_content, + ) + _ = datetime.datetime.now() + datetime.timedelta(hours=8) + formatted_send_time = _.strftime(TIME_FORMAT + "Z") + logger.info( + "Email sent to %s at %s", + row.get("Name", "Unknown"), + formatted_send_time, + ) + return formatted_send_time, "SUCCESS" + except Exception as e: + logger.error("Failed to send email to %s: %s", receiver_email, e) + return None, "FAILED" + except Exception as e: + logger.error("Error in send_email: %s", e) + return None, "FAILED" + + +def save_to_dynamodb( + run_id, + email_id, + display_name, + status, + recipient_email, + template_file_id, + spreadsheet_file_id, + created_at, +): + try: + dynamodb = DynamoDB() + table_name = "campaign" + item = { + "run_id": run_id, + "email_id": email_id, + "display_name": display_name, + "status": status, + "recipient_email": recipient_email, + "template_file_id": template_file_id, + "spreadsheet_file_id": spreadsheet_file_id, + "created_at": created_at, + } + dynamodb.put_item(table_name=table_name, item=item) + except ClientError as e: + logger.error("Error in save_to_dynamodb: %s", e) + except Exception as e: + logger.error("Error in save_to_dynamodb: %s", e) + raise + + +def process_email( + email_title, + template_content, + row, + display_name, + run_id, + template_file_id, + spreadsheet_id, +): + email = str(row.get("Email", "")) + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): + logger.warning("Invalid email address provided: %s", email) + return "FAILED", email + send_time, status = send_email(email_title, template_content, row, display_name) + save_to_dynamodb( + run_id, + uuid.uuid4().hex, + display_name, + status, + email, + template_file_id, + spreadsheet_id, + send_time, + ) + return status, email diff --git a/src/campaign_service/terraform/.terraform.lock.hcl b/src/campaign_service/terraform/.terraform.lock.hcl index 3d25ee8..475bdde 100644 --- a/src/campaign_service/terraform/.terraform.lock.hcl +++ b/src/campaign_service/terraform/.terraform.lock.hcl @@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/aws" { version = "5.54.1" constraints = ">= 4.22.0, >= 4.40.0, >= 5.32.0, >= 5.37.0, ~> 5.54.0" hashes = [ + "h1:+aq386lQCaPX7wR6EPf3PPZvCiI6dRwnjb1wR6lNa8E=", "h1:h6AA+TgBpDNQXFcLi4xKYiDbn94Dfhz7lt8Q8x8CEI8=", "zh:37c09b9a0a0a2f7854fe52c6adb15f71593810b458a8283ed71d68036af7ba3a", "zh:42fe11d87723d4e43b9c6224ae6bacdcb53faee8abc58f0fc625a161d1f71cb1", @@ -29,6 +30,7 @@ provider "registry.terraform.io/hashicorp/external" { constraints = ">= 1.0.0" hashes = [ "h1:/x65slrvO8YG5MKxE2DaU5udEbUxBu3BgEiO7EEM9bQ=", + "h1:gShzO1rJtADK9tDZMvMgjciVAzsBh39LNjtThCwX1Hg=", "zh:03d81462f9578ec91ce8e26f887e34151eda0e100f57e9772dbea86363588239", "zh:37ec2a20f6a3ec3a0fd95d3f3de26da6cb9534b30488bc45723e118a0911c0d8", "zh:4eb5b119179539f2749ce9de0e1b9629d025990f062f4f4dddc161562bb89d37", @@ -48,6 +50,7 @@ provider "registry.terraform.io/hashicorp/local" { version = "2.5.1" constraints = ">= 1.0.0, ~> 2.5.1" hashes = [ + "h1:/GAVA/xheGQcbOZEq0qxANOg+KVLCA7Wv8qluxhTjhU=", "h1:Np4kERf9SMrqUi7DJ1rK3soMK14k49nfgE7l/ipQ5xw=", "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", @@ -68,6 +71,7 @@ provider "registry.terraform.io/hashicorp/null" { version = "3.2.2" constraints = ">= 2.0.0" hashes = [ + "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", "h1:m467k2tZ9cdFFgHW7LPBK2GLPH43LC6wc3ppxr8yvoE=", "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", @@ -88,6 +92,7 @@ provider "registry.terraform.io/hashicorp/random" { version = "3.6.2" hashes = [ "h1:5lstwe/L8AZS/CP0lil2nPvmbbjAu8kCaU/ogSGNbxk=", + "h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=", "zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec", "zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53", "zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114", @@ -108,6 +113,7 @@ provider "registry.terraform.io/kreuzwerker/docker" { constraints = ">= 3.0.0, ~> 3.0.2" hashes = [ "h1:DcRxJArfX6EiATluWeCBW7HoD6usz9fMoTK2U3dmyPk=", + "h1:XjdpVL61KtTsuPE8swok3GY8A+Bu3TZs8T2DOEpyiXo=", "zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f", "zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95", "zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138", diff --git a/src/campaign_service/terraform/api_gateway.tf b/src/campaign_service/terraform/api_gateway.tf index dba8b38..e36aeb3 100644 --- a/src/campaign_service/terraform/api_gateway.tf +++ b/src/campaign_service/terraform/api_gateway.tf @@ -1,6 +1,6 @@ locals { region = var.aws_region - custom_domain_name = "${var.environment}-${var.service_hyphen}-internal-api-tpet.awseducate.systems" + custom_domain_name = "${var.environment}-${var.service_hyphen}-internal-api-tpet.aws-educate.tw" sub_domain_name = "${var.environment}-${var.service_hyphen}-internal-api-tpet" tags = { @@ -64,7 +64,17 @@ module "api_gateway" { } } - + "GET /confirm-attendance" = { + detailed_metrics_enabled = true + throttling_rate_limit = 80 + throttling_burst_limit = 40 + integration = { + uri = module.confirm_attendance_lambda.lambda_function_arn # Remember to change + type = "AWS_PROXY" + payload_format_version = "1.0" + timeout_milliseconds = 29000 + } + } "$default" = { integration = { diff --git a/src/campaign_service/terraform/dynamodb.tf b/src/campaign_service/terraform/dynamodb.tf index 6602932..b91503f 100644 --- a/src/campaign_service/terraform/dynamodb.tf +++ b/src/campaign_service/terraform/dynamodb.tf @@ -13,6 +13,11 @@ resource "aws_dynamodb_table" "campaign" { type = "S" } + attribute { + name = "participant_id" + type = "S" + } + global_secondary_index { name = "campaign_id-created_at-gsi" hash_key = "campaign_id" @@ -20,6 +25,13 @@ resource "aws_dynamodb_table" "campaign" { projection_type = "ALL" } + global_secondary_index { + name = "campaign_id-participant_id-gsi" + hash_key = "campaign_id" + range_key = "participant_id" + projection_type = "ALL" + } + tags = { Name = "campaign" } diff --git a/src/campaign_service/terraform/lambda.tf b/src/campaign_service/terraform/lambda.tf index 59bcb5a..7f4068c 100644 --- a/src/campaign_service/terraform/lambda.tf +++ b/src/campaign_service/terraform/lambda.tf @@ -13,7 +13,7 @@ resource "random_string" "this" { locals { source_path = "${path.module}/.." create_campaign_function_name_and_ecr_repo_name = "${var.environment}-${var.service_underscore}-create_campaign-${random_string.this.result}" - list_campaigns_function_name_and_ecr_repo_name = "${var.environment}-${var.service_underscore}-list_campaigns-${random_string.this.result}" + confirm_attendance_function_name_and_ecr_repo_name = "${var.environment}-${var.service_underscore}-confirm_attendance-${random_string.this.result}" get_campaign_function_name_and_ecr_repo_name = "${var.environment}-${var.service_underscore}-get_campaign-${random_string.this.result}" path_include = ["**"] path_exclude = ["**/__pycache__/**"] @@ -33,7 +33,7 @@ provider "docker" { #################################### #################################### #################################### -# POST /campaigns ####### +# POST /campaigns ################## #################################### #################################### #################################### @@ -51,7 +51,7 @@ module "create_campaign_lambda" { # Container Image ################## package_type = "Image" - architectures = ["x86_64"] # or ["arm64"] + architectures = ["arm64"] # or ["arm64"] image_uri = module.create_campaign_docker_image.image_uri publish = true # Whether to publish creation/change as new Lambda Function Version. @@ -134,3 +134,114 @@ module "create_campaign_docker_image" { } +#################################### +#################################### +#################################### +# GET /confirm_attendance ################# +#################################### +#################################### +#################################### + +module "confirm_attendance_lambda" { + source = "terraform-aws-modules/lambda/aws" + version = "7.7.0" + + function_name = "${var.environment}-${var.service_underscore}-confirm_attendance-${random_string.this.result}" + description = "AWS Educate TPET ${var.service_hyphen} in ${var.environment}: GET /confirm_attendance" + create_package = false + timeout = 300 + + ################## + # Container Image + ################## + package_type = "Image" + architectures = ["arm64"] # or ["arm64"] + image_uri = module.confirm_attendance_docker_image.image_uri + + publish = true # Whether to publish creation/change as new Lambda Function Version. + + environment_variables = { + "ENVIRONMENT" = var.environment, + "SERVICE" = var.service_underscore, + "DYNAMODB_TABLE" = var.dynamodb_table + } + + allowed_triggers = { + AllowExecutionFromAPIGateway = { + service = "apigateway" + source_arn = "${module.api_gateway.api_execution_arn}/*/*" + } + } + + tags = { + "Terraform" = "true", + "Environment" = var.environment, + "Service" = var.service_underscore + } + + ###################### + # Additional policies + ###################### + attach_policy_statements = true + policy_statements = { + dynamodb_crud = { + effect = "Allow", + actions = [ + "dynamodb:BatchGetItem", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:UpdateItem" + ], + resources = [ + "arn:aws:dynamodb:${var.aws_region}:${data.aws_caller_identity.this.account_id}:table/${var.dynamodb_table}" + ] + }, + ses_send_email = { + effect = "Allow", + actions = [ + "ses:SendEmail" + ], + resources = [ + "*" + ] + } + } +} + +module "confirm_attendance_docker_image" { + source = "terraform-aws-modules/lambda/aws//modules/docker-build" + version = "7.7.0" + + create_ecr_repo = true + keep_remotely = true + use_image_tag = false + image_tag_mutability = "MUTABLE" + ecr_repo = "${var.environment}-${var.service_underscore}-confirm_attendance-${random_string.this.result}" + ecr_repo_lifecycle_policy = jsonencode({ + "rules" : [ + { + "rulePriority" : 1, + "description" : "Keep only the last 10 images", + "selection" : { + "tagStatus" : "any", + "countType" : "imageCountMoreThan", + "countNumber" : 10 + }, + "action" : { + "type" : "expire" + } + } + ] + }) + + # docker_campaign_path = "${local.source_path}/path/to/Dockercampaign" # set `docker_campaign_path` If your Dockercampaign is not in `source_path` + source_path = "${local.source_path}/confirm_attendance/" # Remember to change + triggers = { + dir_sha = local.dir_sha + } +} + diff --git a/src/campaign_service/terraform/variables.tf b/src/campaign_service/terraform/variables.tf index 8247229..d4d60f9 100644 --- a/src/campaign_service/terraform/variables.tf +++ b/src/campaign_service/terraform/variables.tf @@ -15,7 +15,7 @@ variable "service_hyphen" { } variable "domain_name" { description = "Domain name, for example: example.com" - default = "awseducate.systems" + default = "aws-educate.tw" } variable "dynamodb_table" { diff --git a/test_jira_connect_github.txt b/test_jira_connect_github.txt new file mode 100644 index 0000000..28511df --- /dev/null +++ b/test_jira_connect_github.txt @@ -0,0 +1,5 @@ +Hello Jira! +Hello GitHub! +Hello Slack! +Hellow Slack and Jira +Test \ No newline at end of file