From a68ace3f657635dcb3e948cb6af4bebc57dba6c6 Mon Sep 17 00:00:00 2001 From: Jordan Sim-Smith Date: Sun, 3 Nov 2024 20:39:26 +0900 Subject: [PATCH] Deploy price tracker on a 1 hour schedule --- immersion_tracker_api/infra/main.tf | 1 - .../SnsNotificationPublisher.java | 12 +- price_tracker_api/BUILD.bazel | 2 + price_tracker_api/infra/main.tf | 131 +++++++++++++++++- .../pricetracker/ProductsFactoryImpl.java | 10 +- .../pricetracker/UpdatePricesHandler.java | 4 +- 6 files changed, 153 insertions(+), 7 deletions(-) diff --git a/immersion_tracker_api/infra/main.tf b/immersion_tracker_api/infra/main.tf index 7898b07..7773b32 100644 --- a/immersion_tracker_api/infra/main.tf +++ b/immersion_tracker_api/infra/main.tf @@ -129,7 +129,6 @@ data "aws_iam_policy_document" "lambda_dynamodb_allow_policy_document" { actions = [ "dynamodb:PutItem", "dynamodb:UpdateItem", - "dynamodb:DeleteItem", "dynamodb:BatchWriteItem", "dynamodb:GetItem", "dynamodb:BatchGetItem", diff --git a/lib/notifications/src/main/java/com/jordansimsmith/lib/notifications/SnsNotificationPublisher.java b/lib/notifications/src/main/java/com/jordansimsmith/lib/notifications/SnsNotificationPublisher.java index 05e0243..d1ee0cc 100644 --- a/lib/notifications/src/main/java/com/jordansimsmith/lib/notifications/SnsNotificationPublisher.java +++ b/lib/notifications/src/main/java/com/jordansimsmith/lib/notifications/SnsNotificationPublisher.java @@ -1,6 +1,8 @@ package com.jordansimsmith.lib.notifications; +import com.google.common.collect.Iterables; import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.model.ListTopicsRequest; import software.amazon.awssdk.services.sns.model.PublishRequest; public class SnsNotificationPublisher implements NotificationPublisher { @@ -12,8 +14,16 @@ public SnsNotificationPublisher(SnsClient snsClient) { @Override public void publish(String topic, String subject, String message) { - var req = PublishRequest.builder().topicArn(topic).subject(subject).message(message).build(); + var topicArn = getTopicArn(topic); + var req = PublishRequest.builder().topicArn(topicArn).subject(subject).message(message).build(); snsClient.publish(req); } + + private String getTopicArn(String topic) { + var req = ListTopicsRequest.builder().build(); + var res = snsClient.listTopics(req); + var topics = res.topics().stream().filter(t -> t.topicArn().endsWith(":" + topic)).toList(); + return Iterables.getOnlyElement(topics).topicArn(); + } } diff --git a/price_tracker_api/BUILD.bazel b/price_tracker_api/BUILD.bazel index 0dd9d11..1e61ca2 100644 --- a/price_tracker_api/BUILD.bazel +++ b/price_tracker_api/BUILD.bazel @@ -36,6 +36,8 @@ java_binary( ":lib", "//lib/notifications:lib", "//lib/time:lib", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", "@maven//:com_amazonaws_aws_lambda_java_core", "@maven//:com_amazonaws_aws_lambda_java_events", "@maven//:com_google_code_findbugs_jsr305", diff --git a/price_tracker_api/infra/main.tf b/price_tracker_api/infra/main.tf index 21fec41..063fac4 100644 --- a/price_tracker_api/infra/main.tf +++ b/price_tracker_api/infra/main.tf @@ -31,6 +31,8 @@ locals { handler = "com.jordansimsmith.pricetracker.UpdatePricesHandler" } } + + subscriptions = ["jordansimsmith@gmail.com"] } data "aws_iam_policy_document" "lambda_sts_allow_policy_document" { @@ -57,6 +59,112 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" { policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } +resource "aws_dynamodb_table" "price_tracker" { + name = "price_tracker" + billing_mode = "PAY_PER_REQUEST" + hash_key = "pk" + range_key = "sk" + + attribute { + name = "pk" + type = "S" + } + + attribute { + name = "sk" + type = "S" + } + + point_in_time_recovery { + enabled = true + } + + deletion_protection_enabled = true + + tags = local.tags +} + +data "aws_iam_policy_document" "lambda_dynamodb" { + statement { + effect = "Allow" + + resources = [ + aws_dynamodb_table.price_tracker.arn + ] + + actions = [ + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:BatchWriteItem", + "dynamodb:GetItem", + "dynamodb:BatchGetItem", + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:ConditionCheckItem", + ] + } +} + +resource "aws_iam_policy" "lambda_dynamodb" { + name = "${local.application_id}_lambda_dynamodb" + policy = data.aws_iam_policy_document.lambda_dynamodb.json + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "lambda_dynamodb" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.lambda_dynamodb.arn +} + +resource "aws_sns_topic" "price_updates" { + name = "${local.application_id}_price_updates" +} + +resource "aws_sns_topic_subscription" "price_updates" { + for_each = toset(local.subscriptions) + topic_arn = aws_sns_topic.price_updates.arn + protocol = "email" + endpoint = each.value +} + +data "aws_iam_policy_document" "lambda_sns" { + statement { + effect = "Allow" + + resources = [ + aws_sns_topic.price_updates.arn, + ] + + actions = [ + "SNS:Publish", + "SNS:GetTopicAttributes", + ] + } + + statement { + effect = "Allow" + + resources = [ + "*" + ] + + actions = [ + "SNS:ListTopics" + ] + } +} + +resource "aws_iam_policy" "lambda_sns" { + name = "${local.application_id}_lambda_sns" + policy = data.aws_iam_policy_document.lambda_sns.json + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "lambda_sns" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.lambda_sns.arn +} + data "external" "handler_location" { for_each = local.lambdas @@ -82,7 +190,26 @@ resource "aws_lambda_function" "lambda" { source_code_hash = data.local_file.handler_file[each.key].content_base64sha256 handler = each.value.handler runtime = "java17" - memory_size = 512 - timeout = 10 + memory_size = 1024 + timeout = 30 tags = local.tags } + +resource "aws_cloudwatch_event_rule" "trigger" { + name = "${local.application_id}_trigger" + schedule_expression = "rate(1 hour)" +} + +resource "aws_cloudwatch_event_target" "trigger" { + rule = aws_cloudwatch_event_rule.trigger.name + target_id = "lambda" + arn = aws_lambda_function.lambda["update_prices"].arn +} + +resource "aws_lambda_permission" "cloudwatch_trigger" { + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.lambda["update_prices"].function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.trigger.arn +} \ No newline at end of file diff --git a/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/ProductsFactoryImpl.java b/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/ProductsFactoryImpl.java index 38e7614..f3ad337 100644 --- a/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/ProductsFactoryImpl.java +++ b/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/ProductsFactoryImpl.java @@ -41,6 +41,14 @@ public List findChemistWarehouseProducts() { new Product( URI.create( "https://www.chemistwarehouse.co.nz/buy/111309/inc-high-protein-bar-peanut-butter-fudge-100g"), - "Chemist Warehouse - INC High Protein Bar Peanut Butter Fudge 100g")); + "Chemist Warehouse - INC High Protein Bar Peanut Butter Fudge 100g"), + new Product( + URI.create( + "https://www.chemistwarehouse.co.nz/buy/120088/musashi-electrolytes-blue-raspberry-300g"), + "Musashi Electrolytes Blue Raspberry 300g"), + new Product( + URI.create( + "https://www.chemistwarehouse.co.nz/buy/101969/musashi-electrolytes-watermelon-300g"), + "Musashi Electrolytes Watermelon 300g")); } } diff --git a/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/UpdatePricesHandler.java b/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/UpdatePricesHandler.java index 14f2e8e..6c3605a 100644 --- a/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/UpdatePricesHandler.java +++ b/price_tracker_api/src/main/java/com/jordansimsmith/pricetracker/UpdatePricesHandler.java @@ -15,7 +15,7 @@ import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; public class UpdatePricesHandler implements RequestHandler { - @VisibleForTesting static final String TOPIC = "my topic"; + @VisibleForTesting static final String TOPIC = "price_tracker_api_price_updates"; private final Clock clock; private final NotificationPublisher notificationPublisher; @@ -93,7 +93,7 @@ private Void doHandleRequest(ScheduledEvent event, Context context) throws Excep priceChanges.size() == 1 ? "1 price updated" : "%d prices updated".formatted(priceChanges.size()); - var message = new StringJoiner("\\n"); + var message = new StringJoiner("\r\n\r\n"); for (var priceChange : priceChanges) { var line = "%s $%.2f -> $%.2f %s"