From 977c5de661ecf0b2ca816bd462e8d9c9d52c087d Mon Sep 17 00:00:00 2001 From: Kento555 Date: Sat, 8 Mar 2025 02:27:01 +0800 Subject: [PATCH 1/2] first part --- .gitignore | 78 ++++----- .terraform.lock.hcl | 5 +- README.md | 38 ++--- acm.tf | 12 ++ apigateway.tf | 148 ++++++++++++----- dynamodb.tf | 22 +-- lambda.tf | 154 ++++++++++-------- main.tf | 10 +- outputs.tf | 11 +- package/app.py | 126 +++++++------- provider.tf | 15 +- r53.tf | 35 ++++ send_requests.sh | 63 +++---- sns.tf | 20 +++ test-event-examples/delete-topmovies.json | 10 +- .../get-topmovies-by-year.json | 10 +- test-event-examples/get-topmovies.json | 4 +- test-event-examples/put-topmovies.json | 8 +- 18 files changed, 464 insertions(+), 305 deletions(-) create mode 100644 acm.tf create mode 100644 r53.tf create mode 100644 sns.tf diff --git a/.gitignore b/.gitignore index 1a5664f..2508bcd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,39 @@ -# Local .terraform directories -**/.terraform/* - -# .tfstate files -*.tfstate -*.tfstate.* - -# Crash log files -crash.log -crash.*.log - -# Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject -# to change depending on the environment. -*.tfvars -*.tfvars.json - -# Ignore override files as they are usually used to override resources locally and so -# are not checked in -override.tf -override.tf.json -*_override.tf -*_override.tf.json - -# Ignore transient lock info files created by terraform apply -.terraform.tfstate.lock.info - -# Include override files you do wish to add to version control using negated pattern -# !example_override.tf - -# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* - -# Ignore CLI configuration files -.terraformrc -terraform.rc - -*.zip +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +*.zip diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index a0abfc2..1e9bbd3 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -4,6 +4,7 @@ provider "registry.terraform.io/hashicorp/archive" { version = "2.7.0" hashes = [ + "h1:YkXq4JfcoAW0L4B9ghskZUxYbYAXIPlfSqqVFrAS06U=", "h1:uZO0XsK1RW1mBRn8SsVPXT76pSSbyb4SmA0eLJsOe38=", "zh:04e23bebca7f665a19a032343aeecd230028a3822e546e6f618f24c47ff87f67", "zh:5bb38114238e25c45bf85f5c9f627a2d0c4b98fe44a0837e37d48574385f8dad", @@ -21,8 +22,10 @@ provider "registry.terraform.io/hashicorp/archive" { } provider "registry.terraform.io/hashicorp/aws" { - version = "5.82.2" + version = "5.82.2" + constraints = "~> 5.0" hashes = [ + "h1:RuPaHbllUB8a2TGTyc149wJfoh6zhIEjUvFYKR6iP2E=", "h1:kQr3M8lD6q2CdFAGp/IeXzmkbRdMfCgwzWtFUNTwAZI=", "zh:0262fc96012fb7e173e1b7beadd46dfc25b1dc7eaef95b90e936fc454724f1c8", "zh:397413613d27f4f54d16efcbf4f0a43c059bd8d827fe34287522ae182a992f9b", diff --git a/README.md b/README.md index 97481d6..ba1976c 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# Commands to invoke api -```bash -# Add movie -INVOKE_URL=https://xxxxxxx.amazonaws.com -curl \ - -X PUT \ - -H "Content-Type: application/json" \ - -d '{"year": "2013", "title": "The Amazing Spider"}' \ - ${INVOKE_URL}/topmovies - -# Get movie for a particular year -curl ${INVOKE_URL}/topmovies/2013 - -# Get listing -curl ${INVOKE_URL}/topmovies - -# Delete movie for a particular year -curl -X DELETE ${INVOKE_URL}/topmovies/2013 -``` +# Commands to invoke api +```bash +# Add movie +INVOKE_URL=https://xxxxxxx.amazonaws.com +curl \ + -X PUT \ + -H "Content-Type: application/json" \ + -d '{"year": "2013", "title": "The Amazing Spider"}' \ + ${INVOKE_URL}/topmovies + +# Get movie for a particular year +curl ${INVOKE_URL}/topmovies/2013 + +# Get listing +curl ${INVOKE_URL}/topmovies + +# Delete movie for a particular year +curl -X DELETE ${INVOKE_URL}/topmovies/2013 +``` diff --git a/acm.tf b/acm.tf new file mode 100644 index 0000000..cf659e3 --- /dev/null +++ b/acm.tf @@ -0,0 +1,12 @@ +# resource "aws_acm_certificate" "custom_domain_cert" { +# domain_name = "ws.sctp-sandbox.com" # Replace with your custom domain +# validation_method = "DNS" +# subject_alternative_names = [ +# "www.ws.sctp-sandbox.com" # Optional: add any additional subdomains +# ] + +# tags = { +# Name = "CustomDomainCertificate" +# } +# } + diff --git a/apigateway.tf b/apigateway.tf index fc194b1..ee710c9 100644 --- a/apigateway.tf +++ b/apigateway.tf @@ -1,45 +1,103 @@ -resource "aws_apigatewayv2_api" "http_api" { - name = "${local.name_prefix}-topmovies-api" - protocol_type = "HTTP" -} - -resource "aws_apigatewayv2_stage" "default" { - api_id = aws_apigatewayv2_api.http_api.id - - name = "$default" - auto_deploy = true -} - -resource "aws_apigatewayv2_integration" "apigw_lambda" { - api_id = aws_apigatewayv2_api.http_api.id - - integration_uri = "" # todo: fill with apporpriate value - integration_type = "AWS_PROXY" - integration_method = "POST" - payload_format_version = "2.0" -} - -# resource "aws_apigatewayv2_route" "get_topmovies" { -# # todo: fill with apporpriate value -# } - -# resource "aws_apigatewayv2_route" "get_topmovies_by_year" { -# # todo: fill with apporpriate value -# } - -# resource "aws_apigatewayv2_route" "put_topmovies" { -# # todo: fill with apporpriate value -# } - -# resource "aws_apigatewayv2_route" "delete_topmovies_by_year" { -# # todo: fill with apporpriate value -# } - -resource "aws_lambda_permission" "api_gw" { - statement_id = "AllowExecutionFromAPIGateway" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.http_api_lambda.function_name - principal = "apigateway.amazonaws.com" - - source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*" -} +resource "aws_apigatewayv2_api" "http_api" { + name = "${local.name_prefix}-topmovies-api" + protocol_type = "HTTP" +} + +# Define the API Gateway stage with logging +resource "aws_apigatewayv2_stage" "default" { + api_id = aws_apigatewayv2_api.http_api.id + name = "$default" + auto_deploy = true +} + + +resource "aws_apigatewayv2_integration" "apigw_lambda" { + api_id = aws_apigatewayv2_api.http_api.id + integration_uri = aws_lambda_function.http_api_lambda.invoke_arn # todo: fill with apporpriate value + integration_type = "AWS_PROXY" + integration_method = "POST" + payload_format_version = "2.0" +} + +# GET /topmovies +resource "aws_apigatewayv2_route" "get_topmovies" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "GET /topmovies" + target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" +} + +# GET /topmovies/{year} +resource "aws_apigatewayv2_route" "get_topmovies_by_year" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "GET /topmovies/{year}" + target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" +} + +# PUT /topmovies +resource "aws_apigatewayv2_route" "put_topmovies" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "PUT /topmovies" + target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" +} + +# DELETE /topmovies/{year} +resource "aws_apigatewayv2_route" "delete_topmovies_by_year" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "DELETE /topmovies/{year}" + target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" +} + + +resource "aws_lambda_permission" "api_gw" { + statement_id = "AllowExecutionFromAPIGateway" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.http_api_lambda.function_name + principal = "apigateway.amazonaws.com" + + source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*" +} + +# # API Gateway custom domain +# resource "aws_apigatewayv2_domain_name" "custom_domain" { +# domain_name = "ws.sctp-sandbox.com" # Replace with your custom domain + +# domain_name_configuration { +# certificate_arn = aws_acm_certificate.custom_domain_cert.arn # Replace with your ACM certificate ARN +# endpoint_type = "REGIONAL" +# security_policy = "TLS_1_2" +# } +# depends_on = [aws_acm_certificate_validation.cert_validation] +# } + +# # API Gateway mapping to custom domain +# resource "aws_apigatewayv2_api_mapping" "default" { +# api_id = aws_apigatewayv2_api.http_api.id +# domain_name = aws_apigatewayv2_domain_name.custom_domain.domain_name +# stage = aws_apigatewayv2_stage.default.name +# } + +# # CloudWatch log group for API Gateway with retention of 7 days +# resource "aws_cloudwatch_log_group" "api_gateway_log_group" { +# name = "/aws/apigateway/${aws_apigatewayv2_api.http_api.id}" +# retention_in_days = 7 +# } + + +# Alternative for API route +# # API Routes +# locals { +# routes = [ +# { method = "GET", path = "/topmovies" }, +# { method = "GET", path = "/topmovies/{year}" }, +# { method = "PUT", path = "/topmovies" }, +# { method = "DELETE", path = "/topmovies/{year}" } +# ] +# } + +# resource "aws_apigatewayv2_route" "routes" { +# for_each = { for route in local.routes : "${route.method} ${route.path}" => route } + +# api_id = aws_apigatewayv2_api.http_api.id +# route_key = "${each.value.method} ${each.value.path}" +# target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" +# } diff --git a/dynamodb.tf b/dynamodb.tf index 1b5cb06..a8c1d49 100644 --- a/dynamodb.tf +++ b/dynamodb.tf @@ -1,11 +1,11 @@ -resource "aws_dynamodb_table" "table" { - name = "${local.name_prefix}-topmovies" - billing_mode = "PAY_PER_REQUEST" - hash_key = "year" - - attribute { - name = "year" - type = "" # todo: fill with apporpriate value - } - -} +resource "aws_dynamodb_table" "table" { + name = "${local.name_prefix}-topmovies" + billing_mode = "PAY_PER_REQUEST" + hash_key = "year" + + attribute { + name = "year" + type = "N" # todo: fill with apporpriate value + } + +} diff --git a/lambda.tf b/lambda.tf index eb6d125..1dcb507 100644 --- a/lambda.tf +++ b/lambda.tf @@ -1,69 +1,85 @@ -data "archive_file" "lambda_zip" { - type = "zip" - source_dir = "${path.module}/package" - output_path = "${path.module}/package.zip" -} - -resource "aws_lambda_function" "http_api_lambda" { - filename = data.archive_file.lambda_zip.output_path - function_name = "${local.name_prefix}-topmovies-api" - description = "Lambda function to write to dynamodb" - runtime = "python3.13" - handler = "app.lambda_handler" - source_code_hash = data.archive_file.lambda_zip.output_base64sha256 - role = aws_iam_role.lambda_exec.arn - - environment { - variables = {} # todo: fill with apporpriate value - } -} - -resource "aws_iam_role" "lambda_exec" { - name = "${local.name_prefix}-topmovies-api-executionrole" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Sid = "" - Principal = { - Service = "lambda.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_policy" "lambda_exec_role" { - name = "${local.name_prefix}-topmovies-api-ddbaccess" - - policy = < { +# name = dvo.resource_record_name +# record = dvo.resource_record_value +# type = dvo.resource_record_type +# } +# } + +# zone_id = "Z00541411T1NGPV97B5C0" # Replace with your Route 53 zone ID +# name = each.value.name +# type = each.value.type +# ttl = 60 +# records = [each.value.record] +# } + +# # Wait for ACM certificate validation +# resource "aws_acm_certificate_validation" "cert_validation" { +# certificate_arn = aws_acm_certificate.custom_domain_cert.arn +# validation_record_fqdns = [for record in aws_route53_record.custom_domain_validation : record.fqdn] +# } + +# # Route 53 Record for Custom Domain +# resource "aws_route53_record" "custom_domain_record" { +# zone_id = "Z00541411T1NGPV97B5" # Replace with your Route 53 zone ID +# name = "ws.sctp-sandbox.com" # Replace with your custom domain +# type = "A" + +# alias { +# name = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].target_domain_name +# zone_id = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].hosted_zone_id +# evaluate_target_health = true +# } +# } \ No newline at end of file diff --git a/send_requests.sh b/send_requests.sh index d9511df..adbda3b 100755 --- a/send_requests.sh +++ b/send_requests.sh @@ -1,31 +1,32 @@ -#!/bin/bash - -INVOKE_URL=https://xxxxxxx.amazonaws.com - -# add movies -echo "> add movies" -for i in $(seq 2001 2003); do - json="$(jq -n --arg year "$i" --arg title "MovieTitle$i" '{year: $year, title: $title}')" - curl \ - -X PUT \ - -H "Content-Type: application/json" \ - -d "$json" \ - "$INVOKE_URL/topmovies"; - echo -done - -# get movies by year -echo "> get movies by year" -for i in $(seq 2001 2003); do - curl "$INVOKE_URL/topmovies/$i" - echo -done - -# delete movie -echo "> delete movie from 2002" -curl -X DELETE "$INVOKE_URL/topmovies/2002" -echo - -# get movies -echo "> get movies" -curl "$INVOKE_URL/topmovies" +#!/bin/bash + +# INVOKE_URL=https://m2xaur4tnl.execute-api.us-east-1.amazonaws.com +INVOKE_URL=$(terraform output -raw invoke_url) + +# add movies +echo "> add movies" +for i in $(seq 2001 2003); do + json="$(jq -n --arg year "$i" --arg title "MovieTitle$i" '{year: $year, title: $title}')" + curl \ + -X PUT \ + -H "Content-Type: application/json" \ + -d "$json" \ + "$INVOKE_URL/topmovies"; + echo +done + +# # get movies by year +# echo "> get movies by year" +# for i in $(seq 2001 2003); do +# curl "$INVOKE_URL/topmovies/$i" +# echo +# done + +# # delete movie +# echo "> delete movie from 2002" +# curl -X DELETE "$INVOKE_URL/topmovies/2002" +# echo + +# # get movies +# echo "> get movies" +# curl "$INVOKE_URL/topmovies" diff --git a/sns.tf b/sns.tf new file mode 100644 index 0000000..0c9387e --- /dev/null +++ b/sns.tf @@ -0,0 +1,20 @@ +# Create SNS topic for Lambda notifications +resource "aws_sns_topic" "lambda_notification" { + name = "lambda-notification-topic" +} + +# Create an SNS topic subscription (email) +resource "aws_sns_topic_subscription" "email_subscription" { + topic_arn = aws_sns_topic.lambda_notification.arn + protocol = "email" + endpoint = "kentokongweishen@gmail.com" # Replace with your email address +} + +# Lambda permissions to publish to SNS +resource "aws_lambda_permission" "sns_publish" { + statement_id = "AllowSNSPublish" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.http_api_lambda.function_name + principal = "sns.amazonaws.com" + source_arn = aws_sns_topic.lambda_notification.arn +} \ No newline at end of file diff --git a/test-event-examples/delete-topmovies.json b/test-event-examples/delete-topmovies.json index ee3b19a..fbe86f1 100644 --- a/test-event-examples/delete-topmovies.json +++ b/test-event-examples/delete-topmovies.json @@ -1,6 +1,6 @@ -{ - "routeKey": "DELETE /topmovies/{year}", - "pathParameters": { - "year": "2013" - } +{ + "routeKey": "DELETE /topmovies/{year}", + "pathParameters": { + "year": "2013" + } } \ No newline at end of file diff --git a/test-event-examples/get-topmovies-by-year.json b/test-event-examples/get-topmovies-by-year.json index e729088..20ce25b 100644 --- a/test-event-examples/get-topmovies-by-year.json +++ b/test-event-examples/get-topmovies-by-year.json @@ -1,6 +1,6 @@ -{ - "routeKey": "GET /topmovies/{year}", - "pathParameters": { - "year": "2013" - } +{ + "routeKey": "GET /topmovies/{year}", + "pathParameters": { + "year": "2013" + } } \ No newline at end of file diff --git a/test-event-examples/get-topmovies.json b/test-event-examples/get-topmovies.json index fc6d436..6708e8a 100644 --- a/test-event-examples/get-topmovies.json +++ b/test-event-examples/get-topmovies.json @@ -1,3 +1,3 @@ -{ - "routeKey": "GET /topmovies" +{ + "routeKey": "GET /topmovies" } \ No newline at end of file diff --git a/test-event-examples/put-topmovies.json b/test-event-examples/put-topmovies.json index 135e091..ab74bd5 100644 --- a/test-event-examples/put-topmovies.json +++ b/test-event-examples/put-topmovies.json @@ -1,4 +1,4 @@ -{ - "routeKey": "PUT /topmovies", - "body": "{\"year\": \"2014\", \"title\": \"The Amazing Spider\"}" -} +{ + "routeKey": "PUT /topmovies", + "body": "{\"year\": \"2014\", \"title\": \"The Amazing Spider\"}" +} From 762a798ac670e86e38d44ae1e9d7e8696b63de04 Mon Sep 17 00:00:00 2001 From: Kento555 Date: Sat, 8 Mar 2025 17:32:53 +0800 Subject: [PATCH 2/2] final --- README.md | 11 ++++- acm.tf | 20 ++++----- apigateway.tf | 55 +++++++++++++----------- image-1.png | Bin 0 -> 6308 bytes image.png | Bin 0 -> 29597 bytes lambda.tf | 19 +++++---- package/app.py | 109 ++++++++++++++++++++++++++++++----------------- r53.tf | 62 +++++++++++++-------------- send_requests.sh | 65 ++++++++++++++-------------- sns.tf | 29 +++++++++---- 10 files changed, 215 insertions(+), 155 deletions(-) create mode 100644 image-1.png create mode 100644 image.png diff --git a/README.md b/README.md index ba1976c..921a07a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ INVOKE_URL=https://xxxxxxx.amazonaws.com curl \ -X PUT \ -H "Content-Type: application/json" \ - -d '{"year": "2013", "title": "The Amazing Spider"}' \ + -d '{"year": "2025", "title": "The Avengers"}' \ ${INVOKE_URL}/topmovies # Get movie for a particular year @@ -17,3 +17,12 @@ curl ${INVOKE_URL}/topmovies # Delete movie for a particular year curl -X DELETE ${INVOKE_URL}/topmovies/2013 ``` + + +# Missing dependancy Error: +![alt text](image-1.png) + +Resolution: +![alt text](image.png) + + diff --git a/acm.tf b/acm.tf index cf659e3..f232551 100644 --- a/acm.tf +++ b/acm.tf @@ -1,12 +1,12 @@ -# resource "aws_acm_certificate" "custom_domain_cert" { -# domain_name = "ws.sctp-sandbox.com" # Replace with your custom domain -# validation_method = "DNS" -# subject_alternative_names = [ -# "www.ws.sctp-sandbox.com" # Optional: add any additional subdomains -# ] +resource "aws_acm_certificate" "custom_domain_cert" { + domain_name = "ws-api.sctp-sandbox.com" # Replace with your custom domain + validation_method = "DNS" + subject_alternative_names = [ + "www.ws-api.sctp-sandbox.com" # Optional: add any additional subdomains + ] -# tags = { -# Name = "CustomDomainCertificate" -# } -# } + tags = { + Name = "CustomDomainCertificate" + } +} diff --git a/apigateway.tf b/apigateway.tf index ee710c9..88641a6 100644 --- a/apigateway.tf +++ b/apigateway.tf @@ -1,3 +1,4 @@ +# Creates an AWS API Gateway v2 (HTTP API) resource "aws_apigatewayv2_api" "http_api" { name = "${local.name_prefix}-topmovies-api" protocol_type = "HTTP" @@ -10,15 +11,16 @@ resource "aws_apigatewayv2_stage" "default" { auto_deploy = true } - +# Intergrate API Gateway with Lambda function resource "aws_apigatewayv2_integration" "apigw_lambda" { api_id = aws_apigatewayv2_api.http_api.id - integration_uri = aws_lambda_function.http_api_lambda.invoke_arn # todo: fill with apporpriate value + integration_uri = aws_lambda_function.http_api_lambda.invoke_arn integration_type = "AWS_PROXY" integration_method = "POST" payload_format_version = "2.0" } +# Create API routes: # GET /topmovies resource "aws_apigatewayv2_route" "get_topmovies" { api_id = aws_apigatewayv2_api.http_api.id @@ -47,7 +49,7 @@ resource "aws_apigatewayv2_route" "delete_topmovies_by_year" { target = "integrations/${aws_apigatewayv2_integration.apigw_lambda.id}" } - +# Grants API Gateway permission to invoke the Lambda function. resource "aws_lambda_permission" "api_gw" { statement_id = "AllowExecutionFromAPIGateway" action = "lambda:InvokeFunction" @@ -57,33 +59,36 @@ resource "aws_lambda_permission" "api_gw" { source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*" } -# # API Gateway custom domain -# resource "aws_apigatewayv2_domain_name" "custom_domain" { -# domain_name = "ws.sctp-sandbox.com" # Replace with your custom domain +# Create API Gateway custom domain +resource "aws_apigatewayv2_domain_name" "custom_domain" { + domain_name = "ws-api.sctp-sandbox.com" # Replace with your custom domain + + domain_name_configuration { + certificate_arn = aws_acm_certificate.custom_domain_cert.arn + endpoint_type = "REGIONAL" + security_policy = "TLS_1_2" + } + depends_on = [aws_acm_certificate_validation.cert_validation] +} + +# API Gateway mapping to custom domain +resource "aws_apigatewayv2_api_mapping" "default" { + api_id = aws_apigatewayv2_api.http_api.id + domain_name = aws_apigatewayv2_domain_name.custom_domain.domain_name + stage = aws_apigatewayv2_stage.default.name +} + +# CloudWatch log group for API Gateway with retention of 7 days +resource "aws_cloudwatch_log_group" "api_gateway_log_group" { + name = "/aws/apigateway/${aws_apigatewayv2_api.http_api.id}" + retention_in_days = 7 +} -# domain_name_configuration { -# certificate_arn = aws_acm_certificate.custom_domain_cert.arn # Replace with your ACM certificate ARN -# endpoint_type = "REGIONAL" -# security_policy = "TLS_1_2" -# } -# depends_on = [aws_acm_certificate_validation.cert_validation] -# } -# # API Gateway mapping to custom domain -# resource "aws_apigatewayv2_api_mapping" "default" { -# api_id = aws_apigatewayv2_api.http_api.id -# domain_name = aws_apigatewayv2_domain_name.custom_domain.domain_name -# stage = aws_apigatewayv2_stage.default.name -# } -# # CloudWatch log group for API Gateway with retention of 7 days -# resource "aws_cloudwatch_log_group" "api_gateway_log_group" { -# name = "/aws/apigateway/${aws_apigatewayv2_api.http_api.id}" -# retention_in_days = 7 -# } -# Alternative for API route +# # Alternative for API route # # API Routes # locals { # routes = [ diff --git a/image-1.png b/image-1.png new file mode 100644 index 0000000000000000000000000000000000000000..1d408879b063ea9980734de11e129f326030e9b0 GIT binary patch literal 6308 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 H8~gnqpc_ps literal 0 HcmV?d00001 diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..26d0dbd3c7fd5ef655ed808547009d425d88d28e GIT binary patch literal 29597 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 IW#Ie20LHK1;Q#;t literal 0 HcmV?d00001 diff --git a/lambda.tf b/lambda.tf index 1dcb507..03a021b 100644 --- a/lambda.tf +++ b/lambda.tf @@ -16,11 +16,12 @@ resource "aws_lambda_function" "http_api_lambda" { environment { variables = { DDB_TABLE = aws_dynamodb_table.table.name - SNS_TOPIC_ARN = aws_sns_topic.lambda_notification.arn # Include SNS topic ARN in Lambda environment variables + SNS_TOPIC_ARN = aws_sns_topic.lambda_notification.arn # Include SNS topic ARN in Lambda environment variables } } } + resource "aws_iam_role" "lambda_exec" { name = "${local.name_prefix}-topmovies-api-executionrole" @@ -74,12 +75,12 @@ resource "aws_iam_role_policy_attachment" "lambda_policy" { policy_arn = aws_iam_policy.lambda_exec_role.arn } -# # Define the CloudWatch log group for Lambda with retention of 7 days -# resource "aws_cloudwatch_log_group" "lambda_log_group" { -# name = "/aws/lambda/${aws_lambda_function.http_api_lambda.function_name}" -# retention_in_days = 7 +# Define the CloudWatch log group for Lambda with retention of 7 days +resource "aws_cloudwatch_log_group" "lambda_log_group" { + name = "/aws/lambda/${aws_lambda_function.http_api_lambda.function_name}-unique-suffix" + retention_in_days = 7 -# lifecycle { -# ignore_changes = [name] -# } -# } \ No newline at end of file + lifecycle { + ignore_changes = [name] + } +} \ No newline at end of file diff --git a/package/app.py b/package/app.py index 7f77f22..dd9b2d9 100644 --- a/package/app.py +++ b/package/app.py @@ -3,57 +3,90 @@ import json import logging +# Configure logging logger = logging.getLogger() logger.setLevel(logging.INFO) +# Load environment variables table_name = os.environ.get('DDB_TABLE') -logging.info(f"## Loaded table name from environemt variable DDB_TABLE: {table_name}") +sns_topic_arn = os.environ.get('SNS_TOPIC_ARN') +logging.info(f"## Loaded table name from environment variable DDB_TABLE: {table_name}") +logging.info(f"## Loaded SNS topic ARN from environment variable SNS_TOPIC_ARN: {sns_topic_arn}") + +# Initialize AWS clients dynamodb_client = boto3.client('dynamodb') dynamodb_resource = boto3.resource("dynamodb") +sns_client = boto3.client('sns') + table = dynamodb_resource.Table(table_name) -logging.info(f"## Initialized table") +logging.info(f"## Initialized DynamoDB table") + +def send_sns_notification(message): + """Publish a message to SNS topic.""" + try: + response = sns_client.publish( + TopicArn=sns_topic_arn, + Message=json.dumps(message), + Subject="Lambda Notification" + ) + logging.info(f"## SNS publish response: {response}") + except Exception as e: + logging.error(f"## Failed to publish SNS notification: {str(e)}") def lambda_handler(event, context): logging.info(f"event: {event}") - logging.info(f"routeKey: {event['routeKey']}") - - if event['routeKey'] == "DELETE /topmovies/{year}": - year = int(event['pathParameters']['year']) - query_result = table.delete_item(Key={ 'year': year }) - logging.info(f"query_result: {query_result}") - body = {"message": f"deleted top movie for {year}"} - - elif event['routeKey'] == "GET /topmovies/{year}": - year = int(event['pathParameters']['year']) - query_result = table.get_item(Key={ 'year': year }) - logging.info(f"query_result: {query_result}") - if "Item" in query_result.keys(): - item = query_result["Item"] - body = {"year": int(item['year']), "title": item['title']} + logging.info(f"routeKey: {event.get('routeKey', 'UNKNOWN')}") + + body = {} + + try: + if event['routeKey'] == "DELETE /topmovies/{year}": + year = int(event['pathParameters']['year']) + query_result = table.delete_item(Key={'year': year}) + logging.info(f"query_result: {query_result}") + body = {"message": f"Deleted top movie for {year}"} + send_sns_notification(body) + + elif event['routeKey'] == "GET /topmovies/{year}": + year = int(event['pathParameters']['year']) + query_result = table.get_item(Key={'year': year}) + logging.info(f"query_result: {query_result}") + if "Item" in query_result.keys(): + item = query_result["Item"] + body = {"year": int(item['year']), "title": item['title']} + else: + body = {"message": f"No top movie found for {year}"} + send_sns_notification(body) + + elif event['routeKey'] == "GET /topmovies": + query_result = table.scan() + logging.info(f"query_result: {query_result}") + items = query_result.get("Items", []) + body = [{"year": int(item['year']), "title": item['title']} for item in items] + send_sns_notification({"message": "Fetched all top movies", "movies": body}) + + elif event['routeKey'] == "PUT /topmovies": + request_body = json.loads(event.get('body', '{}')) + logging.info(f"request_body: {request_body}") + year = int(request_body["year"]) + title = request_body["title"] + query_result = table.put_item(Item={"year": year, "title": title}) + logging.info(f"query_result: {query_result}") + body = {"message": f"Added top movie for {year}"} + send_sns_notification(body) + else: - body = {"message": f"no top movie for {year}"} - - elif event['routeKey'] == "GET /topmovies": - query_result = table.scan() - logging.info(f"query_result: {query_result}") - items = query_result["Items"] - body = [{"year": int(item['year']), "title": item['title']} for item in items] - - elif event['routeKey'] == "PUT /topmovies": - request_body = json.loads(event.get('body')) - logging.info(f"request_body: {request_body}") - year = int(request_body["year"]) - title = request_body["title"] - query_result = table.put_item(Item={ "year": year, "title": title }) - logging.info(f"query_result: {query_result}") - body = {"message": f"added top movie for {year}"} - - else: - body = {} - - logging.info(f"body: {body}") + body = {"message": "Invalid route"} + logging.warning(f"## Invalid routeKey: {event.get('routeKey')}") + + except Exception as e: + logging.error(f"## Error processing request: {str(e)}") + body = {"error": str(e)} + response_body = json.dumps(body) + logging.info(f"Response body: {response_body}") + return { "statusCode": 200, "headers": { diff --git a/r53.tf b/r53.tf index e0dcd37..a2f0fe3 100644 --- a/r53.tf +++ b/r53.tf @@ -1,35 +1,35 @@ -# # Route 53 Validation Records -# resource "aws_route53_record" "custom_domain_validation" { -# for_each = { -# for dvo in aws_acm_certificate.custom_domain_cert.domain_validation_options : dvo.domain_name => { -# name = dvo.resource_record_name -# record = dvo.resource_record_value -# type = dvo.resource_record_type -# } -# } +# Create DNS Validation Records in Route 53 +resource "aws_route53_record" "custom_domain_validation" { + for_each = { + for dvo in aws_acm_certificate.custom_domain_cert.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } -# zone_id = "Z00541411T1NGPV97B5C0" # Replace with your Route 53 zone ID -# name = each.value.name -# type = each.value.type -# ttl = 60 -# records = [each.value.record] -# } + zone_id = "Z00541411T1NGPV97B5C0" # Replace with your Route 53 zone ID + name = each.value.name + type = each.value.type + ttl = 60 + records = [each.value.record] +} -# # Wait for ACM certificate validation -# resource "aws_acm_certificate_validation" "cert_validation" { -# certificate_arn = aws_acm_certificate.custom_domain_cert.arn -# validation_record_fqdns = [for record in aws_route53_record.custom_domain_validation : record.fqdn] -# } +# Wait for ACM certificate validation +resource "aws_acm_certificate_validation" "cert_validation" { + certificate_arn = aws_acm_certificate.custom_domain_cert.arn + validation_record_fqdns = [for record in aws_route53_record.custom_domain_validation : record.fqdn] +} -# # Route 53 Record for Custom Domain -# resource "aws_route53_record" "custom_domain_record" { -# zone_id = "Z00541411T1NGPV97B5" # Replace with your Route 53 zone ID -# name = "ws.sctp-sandbox.com" # Replace with your custom domain -# type = "A" +# Route 53 Record for Custom Domain +resource "aws_route53_record" "custom_domain_record" { + zone_id = "Z00541411T1NGPV97B5C0" # Route 53 zone ID + name = "ws-api.sctp-sandbox.com" # custom domain + type = "A" -# alias { -# name = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].target_domain_name -# zone_id = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].hosted_zone_id -# evaluate_target_health = true -# } -# } \ No newline at end of file + alias { + name = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].target_domain_name + zone_id = aws_apigatewayv2_domain_name.custom_domain.domain_name_configuration[0].hosted_zone_id + evaluate_target_health = true + } +} \ No newline at end of file diff --git a/send_requests.sh b/send_requests.sh index adbda3b..269b066 100755 --- a/send_requests.sh +++ b/send_requests.sh @@ -1,32 +1,33 @@ -#!/bin/bash - -# INVOKE_URL=https://m2xaur4tnl.execute-api.us-east-1.amazonaws.com -INVOKE_URL=$(terraform output -raw invoke_url) - -# add movies -echo "> add movies" -for i in $(seq 2001 2003); do - json="$(jq -n --arg year "$i" --arg title "MovieTitle$i" '{year: $year, title: $title}')" - curl \ - -X PUT \ - -H "Content-Type: application/json" \ - -d "$json" \ - "$INVOKE_URL/topmovies"; - echo -done - -# # get movies by year -# echo "> get movies by year" -# for i in $(seq 2001 2003); do -# curl "$INVOKE_URL/topmovies/$i" -# echo -# done - -# # delete movie -# echo "> delete movie from 2002" -# curl -X DELETE "$INVOKE_URL/topmovies/2002" -# echo - -# # get movies -# echo "> get movies" -# curl "$INVOKE_URL/topmovies" +#!/bin/bash +# INVOKE_URL=https://n533ykbcil.execute-api.us-east-1.amazonaws.com +# INVOKE_URL=ws.sctp-sandbox.com + +INVOKE_URL=$(terraform output -raw invoke_url) + +# add movies +echo "> add movies" +for i in $(seq 1990 1995); do + json="$(jq -n --arg year "$i" --arg title "MovieTitle$i" '{year: $year, title: $title}')" + curl \ + -X PUT \ + -H "Content-Type: application/json" \ + -d "$json" \ + "$INVOKE_URL/topmovies"; + echo +done + +# get movies by year +echo "> get movies by year" +for i in $(seq 1990 1995); do + curl "$INVOKE_URL/topmovies/$i" + echo +done + +# # delete movie +# echo "> delete movie from 2002" +# curl -X DELETE "$INVOKE_URL/topmovies/2002" +# echo + +# get movies +echo "> get movies" +curl "$INVOKE_URL/topmovies" diff --git a/sns.tf b/sns.tf index 0c9387e..6753e2e 100644 --- a/sns.tf +++ b/sns.tf @@ -1,6 +1,6 @@ # Create SNS topic for Lambda notifications resource "aws_sns_topic" "lambda_notification" { - name = "lambda-notification-topic" + name = "${local.name_prefix}-lambda-notification-topic" } # Create an SNS topic subscription (email) @@ -10,11 +10,22 @@ resource "aws_sns_topic_subscription" "email_subscription" { endpoint = "kentokongweishen@gmail.com" # Replace with your email address } -# Lambda permissions to publish to SNS -resource "aws_lambda_permission" "sns_publish" { - statement_id = "AllowSNSPublish" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.http_api_lambda.function_name - principal = "sns.amazonaws.com" - source_arn = aws_sns_topic.lambda_notification.arn -} \ No newline at end of file +resource "aws_iam_policy" "lambda_sns_policy" { + name = "${local.name_prefix}-LambdaSNSPublishPolicy" + description = "Allow Lambda to publish to SNS" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = "sns:Publish" + Resource = aws_sns_topic.lambda_notification.arn + }] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_sns_attach" { + role = aws_iam_role.lambda_exec.name + policy_arn = aws_iam_policy.lambda_sns_policy.arn +} +