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..921a07a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ -# 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": "2025", "title": "The Avengers"}' \ + ${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 +``` + + +# Missing dependancy Error: +![alt text](image-1.png) + +Resolution: +![alt text](image.png) + + diff --git a/acm.tf b/acm.tf new file mode 100644 index 0000000..f232551 --- /dev/null +++ b/acm.tf @@ -0,0 +1,12 @@ +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" + } +} + diff --git a/apigateway.tf b/apigateway.tf index fc194b1..88641a6 100644 --- a/apigateway.tf +++ b/apigateway.tf @@ -1,45 +1,108 @@ -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}/*/*" -} +# Creates an AWS API Gateway v2 (HTTP API) +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 +} + +# 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 + 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 + 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}" +} + +# Grants API Gateway permission to invoke the Lambda function. +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}/*/*" +} + +# 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 +} + + + + + +# # 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/image-1.png b/image-1.png new file mode 100644 index 0000000..1d40887 Binary files /dev/null and b/image-1.png differ diff --git a/image.png b/image.png new file mode 100644 index 0000000..26d0dbd Binary files /dev/null and b/image.png differ diff --git a/lambda.tf b/lambda.tf index eb6d125..03a021b 100644 --- a/lambda.tf +++ b/lambda.tf @@ -1,69 +1,86 @@ -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 = "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 diff --git a/send_requests.sh b/send_requests.sh index d9511df..269b066 100755 --- a/send_requests.sh +++ b/send_requests.sh @@ -1,10 +1,12 @@ #!/bin/bash +# INVOKE_URL=https://n533ykbcil.execute-api.us-east-1.amazonaws.com +# INVOKE_URL=ws.sctp-sandbox.com -INVOKE_URL=https://xxxxxxx.amazonaws.com +INVOKE_URL=$(terraform output -raw invoke_url) # add movies echo "> add movies" -for i in $(seq 2001 2003); do +for i in $(seq 1990 1995); do json="$(jq -n --arg year "$i" --arg title "MovieTitle$i" '{year: $year, title: $title}')" curl \ -X PUT \ @@ -16,15 +18,15 @@ done # get movies by year echo "> get movies by year" -for i in $(seq 2001 2003); do +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 +# # delete movie +# echo "> delete movie from 2002" +# curl -X DELETE "$INVOKE_URL/topmovies/2002" +# echo # get movies echo "> get movies" diff --git a/sns.tf b/sns.tf new file mode 100644 index 0000000..6753e2e --- /dev/null +++ b/sns.tf @@ -0,0 +1,31 @@ +# Create SNS topic for Lambda notifications +resource "aws_sns_topic" "lambda_notification" { + name = "${local.name_prefix}-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 +} + +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 +} + 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\"}" +}